diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index ceac3095..035ade57 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -63,7 +63,7 @@ public class AuthController : Controller throw new HttpApiException("No oauth2 provider has been registered", 500); // 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) 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; // 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 return false; diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs index 061acad7..9212cc53 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs @@ -7,6 +7,7 @@ using MoonCore.Services; using Moonlight.ApiServer.Configuration; using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Services; +using Moonlight.Shared.Http.Responses.OAuth2; namespace Moonlight.ApiServer.Http.Controllers.OAuth2; @@ -88,7 +89,7 @@ public class OAuth2Controller : Controller [FromForm(Name = "redirect_uri")] string redirectUri, [FromForm(Name = "grant_type")] string grantType, [FromForm(Name = "code")] string code - ) + ) { if (grantType != "authorization_code") throw new HttpApiException("Invalid grant type", 400); @@ -106,14 +107,56 @@ public class OAuth2Controller : Controller user = UserRepository.Get().FirstOrDefault(x => x.Id == userId); return user != null; - }, data => - { - data.Add("userId", user!.Id.ToString()); - }); + }, data => { data.Add("userId", user!.Id.ToString()); }); if (access == null) throw new HttpApiException("Unable to validate access", 400); return access; } + + [HttpGet("info")] + public async Task 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 + }; + } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs b/Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs new file mode 100644 index 00000000..caf64368 --- /dev/null +++ b/Moonlight.ApiServer/Implementations/OAuth2/LocalOAuth2Provider.cs @@ -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 Sync(IServiceProvider provider, string accessToken) + { + var logger = provider.GetRequiredService>(); + + try + { + var configService = provider.GetRequiredService>(); + 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(); + + var userRepo = provider.GetRequiredService>(); + 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; + } + } +} \ No newline at end of file diff --git a/Moonlight.ApiServer/Interfaces/OAuth2/IOAuth2Provider.cs b/Moonlight.ApiServer/Interfaces/OAuth2/IOAuth2Provider.cs index c0ad5acb..e63ef23e 100644 --- a/Moonlight.ApiServer/Interfaces/OAuth2/IOAuth2Provider.cs +++ b/Moonlight.ApiServer/Interfaces/OAuth2/IOAuth2Provider.cs @@ -4,5 +4,5 @@ namespace Moonlight.ApiServer.Interfaces.OAuth2; public interface IOAuth2Provider { - public Task Sync(IServiceProvider provider, string accessToken, string refreshToken); + public Task Sync(IServiceProvider provider, string accessToken); } \ No newline at end of file diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index a3fcac0c..ca4c332c 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -28,7 +28,6 @@ - diff --git a/Moonlight.ApiServer/Program.cs b/Moonlight.ApiServer/Program.cs index 0f88d774..5ff3c734 100644 --- a/Moonlight.ApiServer/Program.cs +++ b/Moonlight.ApiServer/Program.cs @@ -15,6 +15,8 @@ using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers; using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Http.Middleware; +using Moonlight.ApiServer.Implementations.OAuth2; +using Moonlight.ApiServer.Interfaces.OAuth2; // Prepare file system Directory.CreateDirectory(PathBuilder.Dir("storage")); @@ -97,7 +99,7 @@ builder.Services.AddOAuth2Consumer(configuration => configuration.ClientId = config.Authentication.OAuth2.ClientId; configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret; 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.RefreshEndpoint = config.Authentication.OAuth2.RefreshEndpoint ?? $"{config.PublicUrl}/oauth2/refresh"; @@ -128,7 +130,7 @@ if (config.Authentication.UseLocalOAuth2) configuration.ClientSecret = config.Authentication.OAuth2.ClientSecret; configuration.CodeSecret = config.Authentication.LocalOAuth2.CodeSecret; configuration.AuthorizationRedirect = - config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/api/auth/handle"; + config.Authentication.OAuth2.AuthorizationRedirect ?? $"{config.PublicUrl}/code"; configuration.AccessTokenDuration = 60; configuration.RefreshTokenDuration = 3600; }); @@ -148,33 +150,6 @@ builder.Services.AddTokenAuthentication(configuration => if (user == null) 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(); - - 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 var permissions = JsonSerializer.Deserialize( string.IsNullOrEmpty(user.PermissionsJson) ? "[]" : user.PermissionsJson @@ -214,7 +189,8 @@ if (configService.Get().Development.EnableApiDocs) // Implementation service var implementationService = new ImplementationService(); - +if(config.Authentication.UseLocalOAuth2) + implementationService.Register(); builder.Services.AddSingleton(implementationService); diff --git a/Moonlight.Client/Program.cs b/Moonlight.Client/Program.cs index 9ac53c51..f1a6224e 100644 --- a/Moonlight.Client/Program.cs +++ b/Moonlight.Client/Program.cs @@ -64,9 +64,9 @@ builder.Services.AddScoped(sp => var cookieService = sp.GetRequiredService(); return new TokenConsumer( - await cookieService.GetValue("ml-access"), - await cookieService.GetValue("ml-refresh"), - DateTimeOffset.FromUnixTimeSeconds(long.Parse(await cookieService.GetValue("ml-timestamp"))).UtcDateTime, + await cookieService.GetValue("kms-access", "x"), + await cookieService.GetValue("kms-refresh", "x"), + DateTimeOffset.FromUnixTimeSeconds(long.Parse(await cookieService.GetValue("kms-timestamp", "0"))).UtcDateTime, async refreshToken => { await httpClient.PostAsync("api/auth/refresh", new StringContent( @@ -78,8 +78,8 @@ builder.Services.AddScoped(sp => return new TokenPair() { - AccessToken = await cookieService.GetValue("ml-access"), - RefreshToken = await cookieService.GetValue("ml-refresh") + AccessToken = await cookieService.GetValue("kms-access", "x"), + RefreshToken = await cookieService.GetValue("kms-refresh", "x") }; } ); diff --git a/Moonlight.Client/UI/Screens/AuthenticationScreen.razor b/Moonlight.Client/UI/Screens/AuthenticationScreen.razor index 4f86a28f..f2f61668 100644 --- a/Moonlight.Client/UI/Screens/AuthenticationScreen.razor +++ b/Moonlight.Client/UI/Screens/AuthenticationScreen.razor @@ -1,8 +1,12 @@ +@page "/auth" + @using MoonCore.Helpers +@using Moonlight.Shared.Http.Requests.Auth @using Moonlight.Shared.Http.Responses.Auth @inject NavigationManager Navigation @inject HttpApiClient HttpApiClient +@inject CookieService CookieService
Authenticate @@ -10,9 +14,31 @@ @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("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 _) { - var authStartData = await HttpApiClient.GetJson("api/auth/start"); + var authStartData = await HttpApiClient.GetJson("api/auth"); var uri = authStartData.Endpoint + $"?client_id={authStartData.ClientId}" + diff --git a/Moonlight.Shared/Http/Responses/OAuth2/InfoResponse.cs b/Moonlight.Shared/Http/Responses/OAuth2/InfoResponse.cs new file mode 100644 index 00000000..11b1b08a --- /dev/null +++ b/Moonlight.Shared/Http/Responses/OAuth2/InfoResponse.cs @@ -0,0 +1,7 @@ +namespace Moonlight.Shared.Http.Responses.OAuth2; + +public class InfoResponse +{ + public string Username { get; set; } + public string Email { get; set; } +} \ No newline at end of file