Implemented complete oauth2 flow with modular providers

The local oauth2 provider still needs A LOT of love. But the general oauth2 workflow works. The http api client needs an option to disable the authentication things and we need to switch to the local storage for storing access, refresh and timestamp for the client
This commit is contained in:
Masu Baumgartner
2024-10-19 20:09:03 +02:00
parent 71dc81c4dc
commit f166de1a43
9 changed files with 156 additions and 45 deletions

View File

@@ -63,7 +63,7 @@ public class AuthController : Controller
throw new HttpApiException("No oauth2 provider has been registered", 500); throw new HttpApiException("No oauth2 provider has been registered", 500);
// Sync user from oauth2 provider // Sync user from oauth2 provider
var user = await provider.Sync(HttpContext.RequestServices, accessData.AccessToken, accessData.RefreshToken); var user = await provider.Sync(HttpContext.RequestServices, accessData.AccessToken);
if (user == null) if (user == null)
throw new HttpApiException("The oauth2 provider was unable to authenticate you", 401); throw new HttpApiException("The oauth2 provider was unable to authenticate you", 401);
@@ -172,7 +172,7 @@ public class AuthController : Controller
var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result; var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result;
// Sync user with oauth2 provider // Sync user with oauth2 provider
var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken, refreshData.RefreshToken).Result; var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken).Result;
if (syncedUser == null) // User sync has failed. No refresh allowed if (syncedUser == null) // User sync has failed. No refresh allowed
return false; return false;

View File

@@ -7,6 +7,7 @@ using MoonCore.Services;
using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Services; using Moonlight.ApiServer.Services;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Http.Controllers.OAuth2; namespace Moonlight.ApiServer.Http.Controllers.OAuth2;
@@ -106,14 +107,56 @@ public class OAuth2Controller : Controller
user = UserRepository.Get().FirstOrDefault(x => x.Id == userId); user = UserRepository.Get().FirstOrDefault(x => x.Id == userId);
return user != null; return user != null;
}, data => }, data => { data.Add("userId", user!.Id.ToString()); });
{
data.Add("userId", user!.Id.ToString());
});
if (access == null) if (access == null)
throw new HttpApiException("Unable to validate access", 400); throw new HttpApiException("Unable to validate access", 400);
return access; return access;
} }
[HttpGet("info")]
public async Task<InfoResponse> Info()
{
if (!Request.Headers.ContainsKey("Authorization"))
throw new HttpApiException("Authorization header is missing", 400);
var authHeader = Request.Headers["Authorization"].First() ?? "";
if (string.IsNullOrEmpty(authHeader))
throw new HttpApiException("Authorization header is missing", 400);
User? currentUser = null;
var isValid = await OAuth2Service.IsValidAccessToken(
authHeader,
data =>
{
// Check if the userId is present in the access token
if (!data.TryGetValue("userId", out var userIdStr) || !int.TryParse(userIdStr, out var userId))
return false;
currentUser = UserRepository
.Get()
.FirstOrDefault(x => x.Id == userId);
if (currentUser == null)
return false;
return true;
}
);
if (!isValid)
throw new HttpApiException("Invalid access token", 401);
if(currentUser == null)
throw new HttpApiException("Invalid access token", 401);
return new InfoResponse()
{
Username = currentUser.Username,
Email = currentUser.Email
};
}
} }

View File

@@ -0,0 +1,60 @@
using MoonCore.Extended.Abstractions;
using MoonCore.Extensions;
using MoonCore.Services;
using Moonlight.ApiServer.Configuration;
using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Interfaces.OAuth2;
using Moonlight.Shared.Http.Responses.OAuth2;
namespace Moonlight.ApiServer.Implementations.OAuth2;
public class LocalOAuth2Provider : IOAuth2Provider
{
public async Task<User?> Sync(IServiceProvider provider, string accessToken)
{
var logger = provider.GetRequiredService<ILogger<LocalOAuth2Provider>>();
try
{
var configService = provider.GetRequiredService<ConfigService<AppConfiguration>>();
var config = configService.Get();
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", accessToken);
var response = await httpClient.GetAsync($"{config.PublicUrl}/oauth2/info");
await response.HandlePossibleApiError();
var info = await response.ParseAsJson<InfoResponse>();
var userRepo = provider.GetRequiredService<DatabaseRepository<User>>();
var user = userRepo.Get().FirstOrDefault(x => x.Email == info.Email);
if (user == null) // User not found, register a new one
{
user = userRepo.Add(new User()
{
Email = info.Email,
Username = info.Username
});
}
else if (user.Username != info.Username) // Username updated?
{
// Username not used by another user?
if (!userRepo.Get().Any(x => x.Username == info.Username))
{
// Update username
user.Username = info.Username;
userRepo.Update(user);
}
}
return user;
}
catch (Exception e)
{
logger.LogCritical("Unable to sync user: {e}", e);
return null;
}
}
}

View File

@@ -4,5 +4,5 @@ namespace Moonlight.ApiServer.Interfaces.OAuth2;
public interface IOAuth2Provider public interface IOAuth2Provider
{ {
public Task<User?> Sync(IServiceProvider provider, string accessToken, string refreshToken); public Task<User?> Sync(IServiceProvider provider, string accessToken);
} }

View File

@@ -28,7 +28,6 @@
<ItemGroup> <ItemGroup>
<Folder Include="Database\Migrations\" /> <Folder Include="Database\Migrations\" />
<Folder Include="Implementations\" />
<Folder Include="storage\" /> <Folder Include="storage\" />
</ItemGroup> </ItemGroup>

View File

@@ -15,6 +15,8 @@ using Moonlight.ApiServer.Database.Entities;
using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Helpers;
using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Helpers.Authentication;
using Moonlight.ApiServer.Http.Middleware; using Moonlight.ApiServer.Http.Middleware;
using Moonlight.ApiServer.Implementations.OAuth2;
using Moonlight.ApiServer.Interfaces.OAuth2;
// Prepare file system // Prepare file system
Directory.CreateDirectory(PathBuilder.Dir("storage")); Directory.CreateDirectory(PathBuilder.Dir("storage"));
@@ -97,7 +99,7 @@ builder.Services.AddOAuth2Consumer(configuration =>
configuration.ClientId = config.Authentication.OAuth2.ClientId; configuration.ClientId = config.Authentication.OAuth2.ClientId;
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret; configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.AuthorizationRedirect = configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"; config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/code";
configuration.AccessEndpoint = config.Authentication.OAuth2.AccessEndpoint ?? $"{config.PublicUrl}/oauth2/access"; configuration.AccessEndpoint = config.Authentication.OAuth2.AccessEndpoint ?? $"{config.PublicUrl}/oauth2/access";
configuration.RefreshEndpoint = config.Authentication.OAuth2.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh"; configuration.RefreshEndpoint = config.Authentication.OAuth2.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh";
@@ -128,7 +130,7 @@ if (config.Authentication.UseLocalOAuth2)
configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret; configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret;
configuration.CodeSecret = config.Authentication.LocalOAuth2.CodeSecret; configuration.CodeSecret = config.Authentication.LocalOAuth2.CodeSecret;
configuration.AuthorizationRedirect = configuration.AuthorizationRedirect =
config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"; config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/code";
configuration.AccessTokenDuration = 60; configuration.AccessTokenDuration = 60;
configuration.RefreshTokenDuration = 3600; configuration.RefreshTokenDuration = 3600;
}); });
@@ -148,33 +150,6 @@ builder.Services.AddTokenAuthentication(configuration =>
if (user == null) if (user == null)
return false; return false;
// OAuth2 - Check external
if (false && DateTime.UtcNow > user.RefreshTimestamp)
{
var tokenConsumer = new TokenConsumer(user.AccessToken, user.RefreshToken, user.RefreshTimestamp,
async refreshToken =>
{
var oauth2Service = context.RequestServices.GetRequiredService<OAuth2Service>();
var accessData = await oauth2Service.RefreshAccess(refreshToken);
user.AccessToken = accessData.AccessToken;
user.RefreshToken = accessData.RefreshToken;
user.RefreshTimestamp = DateTime.UtcNow.AddSeconds(accessData.ExpiresIn);
userRepo.Update(user);
return new TokenPair()
{
AccessToken = user.AccessToken,
RefreshToken = user.RefreshToken
};
});
//await tokenConsumer.GetAccessToken();
//TODO: API CALL (modular)
}
// Load permissions, handle empty values // Load permissions, handle empty values
var permissions = JsonSerializer.Deserialize<string[]>( var permissions = JsonSerializer.Deserialize<string[]>(
string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson
@@ -214,7 +189,8 @@ if (configService.Get().Development.EnableApiDocs)
// Implementation service // Implementation service
var implementationService = new ImplementationService(); var implementationService = new ImplementationService();
if(config.Authentication.UseLocalOAuth2)
implementationService.Register<IOAuth2Provider, LocalOAuth2Provider>();
builder.Services.AddSingleton(implementationService); builder.Services.AddSingleton(implementationService);

View File

@@ -64,9 +64,9 @@ builder.Services.AddScoped(sp =>
var cookieService = sp.GetRequiredService<CookieService>(); var cookieService = sp.GetRequiredService<CookieService>();
return new TokenConsumer( return new TokenConsumer(
await cookieService.GetValue("ml-access"), await cookieService.GetValue("kms-access", "x"),
await cookieService.GetValue("ml-refresh"), await cookieService.GetValue("kms-refresh", "x"),
DateTimeOffset.FromUnixTimeSeconds(long.Parse(await cookieService.GetValue("ml-timestamp"))).UtcDateTime, DateTimeOffset.FromUnixTimeSeconds(long.Parse(await cookieService.GetValue("kms-timestamp", "0"))).UtcDateTime,
async refreshToken => async refreshToken =>
{ {
await httpClient.PostAsync("api/auth/refresh", new StringContent( await httpClient.PostAsync("api/auth/refresh", new StringContent(
@@ -78,8 +78,8 @@ builder.Services.AddScoped(sp =>
return new TokenPair() return new TokenPair()
{ {
AccessToken = await cookieService.GetValue("ml-access"), AccessToken = await cookieService.GetValue("kms-access", "x"),
RefreshToken = await cookieService.GetValue("ml-refresh") RefreshToken = await cookieService.GetValue("kms-refresh", "x")
}; };
} }
); );

View File

@@ -1,8 +1,12 @@
@page "/auth"
@using MoonCore.Helpers @using MoonCore.Helpers
@using Moonlight.Shared.Http.Requests.Auth
@using Moonlight.Shared.Http.Responses.Auth @using Moonlight.Shared.Http.Responses.Auth
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject HttpApiClient HttpApiClient @inject HttpApiClient HttpApiClient
@inject CookieService CookieService
<div class="flex justify-center"> <div class="flex justify-center">
<WButton OnClick="StartAuth" CssClasses="btn btn-primary">Authenticate</WButton> <WButton OnClick="StartAuth" CssClasses="btn btn-primary">Authenticate</WButton>
@@ -10,9 +14,31 @@
@code @code
{ {
[SupplyParameterFromQuery(Name = "code")]
[Parameter]
public string? Code { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(!firstRender)
return;
if(Code == null)
return;
var authHandleData = await HttpApiClient.PostJson<OAuth2HandleResponse>("api/auth", new OAuth2HandleRequest()
{
Code = Code
});
await CookieService.SetValue("kms-access", authHandleData.AccessToken, 10);
await CookieService.SetValue("kms-refresh", authHandleData.RefreshToken, 10);
await CookieService.SetValue("kms-timestamp", DateTimeOffset.UtcNow.AddSeconds(60).ToUnixTimeSeconds().ToString(), 10);
}
private async Task StartAuth(WButton _) private async Task StartAuth(WButton _)
{ {
var authStartData = await HttpApiClient.GetJson<OAuth2StartResponse>("api/auth/start"); var authStartData = await HttpApiClient.GetJson<OAuth2StartResponse>("api/auth");
var uri = authStartData.Endpoint var uri = authStartData.Endpoint
+ $"?client_id={authStartData.ClientId}" + + $"?client_id={authStartData.ClientId}" +

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Shared.Http.Responses.OAuth2;
public class InfoResponse
{
public string Username { get; set; }
public string Email { get; set; }
}