From 910f190c86c734b84ec09b04f9e6a4f4dd9362fb Mon Sep 17 00:00:00 2001 From: Masu Baumgartner <68913099+Masu-Baumgartner@users.noreply.github.com> Date: Mon, 21 Oct 2024 20:17:59 +0200 Subject: [PATCH] Completed first iteration of access-refresh auth. Upgraded mooncore. Refactored mooncore related stuff --- .../Configuration/AppConfiguration.cs | 3 + .../Http/Controllers/Auth/AuthController.cs | 77 +++++++++++-------- .../Controllers/OAuth2/OAuth2Controller.cs | 50 +++++++++--- .../Moonlight.ApiServer.csproj | 4 +- Moonlight.ApiServer/Program.cs | 5 +- .../Properties/launchSettings.json | 4 +- Moonlight.ApiServer/Services/AuthService.cs | 5 +- Moonlight.Client/Moonlight.Client.csproj | 2 +- Moonlight.Client/Program.cs | 9 ++- 9 files changed, 103 insertions(+), 56 deletions(-) diff --git a/Moonlight.ApiServer/Configuration/AppConfiguration.cs b/Moonlight.ApiServer/Configuration/AppConfiguration.cs index 699737e7..761e5f03 100644 --- a/Moonlight.ApiServer/Configuration/AppConfiguration.cs +++ b/Moonlight.ApiServer/Configuration/AppConfiguration.cs @@ -38,6 +38,9 @@ public class AppConfiguration public string AccessSecret { get; set; } = Formatter.GenerateString(32); public string RefreshSecret { get; set; } = Formatter.GenerateString(32); public string CodeSecret { get; set; } = Formatter.GenerateString(32); + + public int AccessTokenDuration { get; set; } = 60; + public int RefreshTokenDuration { get; set; } = 3600; } public class OAuth2Data diff --git a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs index aa2b4a6c..c6816159 100644 --- a/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs +++ b/Moonlight.ApiServer/Http/Controllers/Auth/AuthController.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; using MoonCore.Exceptions; using MoonCore.Extended.Abstractions; using MoonCore.Extended.Helpers; @@ -12,7 +13,6 @@ using Moonlight.ApiServer.Database.Entities; using Moonlight.ApiServer.Helpers.Authentication; using Moonlight.ApiServer.Interfaces.Auth; using Moonlight.ApiServer.Interfaces.OAuth2; -using Moonlight.ApiServer.Services; using Moonlight.Shared.Http.Requests.Auth; using Moonlight.Shared.Http.Responses.Auth; @@ -27,20 +27,22 @@ public class AuthController : Controller private readonly ConfigService ConfigService; private readonly DatabaseRepository UserRepository; private readonly ImplementationService ImplementationService; + private readonly ILogger Logger; public AuthController( OAuth2Service oAuth2Service, TokenHelper tokenHelper, DatabaseRepository userRepository, ConfigService configService, - ImplementationService implementationService - ) + ImplementationService implementationService, + ILogger logger) { OAuth2Service = oAuth2Service; TokenHelper = tokenHelper; UserRepository = userRepository; ConfigService = configService; ImplementationService = implementationService; + Logger = logger; } [HttpGet] @@ -54,7 +56,7 @@ public class AuthController : Controller [HttpPost] public async Task Handle([FromBody] OAuth2HandleRequest request) { - var accessData = await OAuth2Service.RequestAccess(request.Code); + var accessData = await OAuth2Service.RequestAccess(request.Code);; // Find oauth2 provider var provider = ImplementationService.Get().FirstOrDefault(); @@ -93,10 +95,10 @@ public class AuthController : Controller var authConfig = ConfigService.Get().Authentication; - var tokenPair = await TokenHelper.GeneratePair( + var tokenPair = TokenHelper.GeneratePair( authConfig.AccessSecret, - authConfig.AccessSecret, - data => { data.Add("userId", user.Id.ToString()); }, + authConfig.RefreshSecret, + data => { data.Add("userId", user.Id); }, authConfig.AccessDuration, authConfig.RefreshDuration ); @@ -116,7 +118,7 @@ public class AuthController : Controller { var authConfig = ConfigService.Get().Authentication; - var tokenPair = await TokenHelper.RefreshPair( + var tokenPair = TokenHelper.RefreshPair( request.RefreshToken, authConfig.AccessSecret, authConfig.RefreshSecret, @@ -139,7 +141,7 @@ public class AuthController : Controller }; } - private bool ProcessRefreshData(Dictionary refreshTokenData, Dictionary newData, IServiceProvider serviceProvider) + private bool ProcessRefreshData(Dictionary refreshTokenData, Dictionary newData, IServiceProvider serviceProvider) { // Find oauth2 provider var provider = ImplementationService.Get().FirstOrDefault(); @@ -148,7 +150,7 @@ public class AuthController : Controller throw new HttpApiException("No oauth2 provider has been registered", 500); // Check if the userId is present in the refresh token - if (!refreshTokenData.TryGetValue("userId", out var userIdStr) || !int.TryParse(userIdStr, out var userId)) + if (!refreshTokenData.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) return false; // Load user from database if existent @@ -166,34 +168,43 @@ public class AuthController : Controller return false; // Check if it's time to resync with the oauth2 provider - if (false && DateTime.UtcNow >= user.RefreshTimestamp) + if (DateTime.UtcNow >= user.RefreshTimestamp) { - // It's time to refresh the access to the external oauth2 provider - var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result; - - // Sync user with oauth2 provider - var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken).Result; + try + { + // It's time to refresh the access to the external oauth2 provider + var refreshData = OAuth2Service.RefreshAccess(user.RefreshToken).Result; - if (syncedUser == null) // User sync has failed. No refresh allowed + // Sync user with oauth2 provider + var syncedUser = provider.Sync(serviceProvider, refreshData.AccessToken).Result; + + if (syncedUser == null) // User sync has failed. No refresh allowed + return false; + + // Save oauth2 refresh and access tokens for later use (re-authentication etc.). + // Fetch user model in current db context, just in case the oauth2 provider + // uses a different db context or smth + + var userModel = UserRepository + .Get() + .First(x => x.Id == syncedUser.Id); + + userModel.AccessToken = refreshData.AccessToken; + userModel.RefreshToken = refreshData.RefreshToken; + userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(refreshData.ExpiresIn); + + UserRepository.Update(userModel); + } + catch (Exception e) + { + // We are handling this error more softly, because it will occur when a user hasn't logged in a long period of time + Logger.LogDebug("An error occured while refreshing external oauth2 access: {e}", e); return false; - - // Save oauth2 refresh and access tokens for later use (re-authentication etc.). - // Fetch user model in current db context, just in case the oauth2 provider - // uses a different db context or smth - - var userModel = UserRepository - .Get() - .First(x => x.Id == syncedUser.Id); - - userModel.AccessToken = refreshData.AccessToken; - userModel.RefreshToken = refreshData.RefreshToken; - userModel.RefreshTimestamp = DateTime.UtcNow.AddSeconds(refreshData.ExpiresIn); - - UserRepository.Update(userModel); + } } // All checks have passed, allow refresh - newData.Add("userId", user.Id.ToString()); + newData.Add("userId", user.Id); return true; } diff --git a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs index 9212cc53..cafbcb03 100644 --- a/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs +++ b/Moonlight.ApiServer/Http/Controllers/OAuth2/OAuth2Controller.cs @@ -74,7 +74,7 @@ public class OAuth2Controller : Controller var user = await AuthService.Login(email, password); - var code = await OAuth2Service.GenerateCode(data => { data.Add("userId", user.Id.ToString()); }); + var code = await OAuth2Service.GenerateCode(data => { data.Add("userId", user.Id); }); var redirectUrl = redirectUri + $"?code={code}"; @@ -98,16 +98,13 @@ public class OAuth2Controller : Controller var access = await OAuth2Service.ValidateAccess(clientId, clientSecret, redirectUri, code, data => { - if (!data.TryGetValue("userId", out var userIdStr)) - return false; - - if (!int.TryParse(userIdStr, out var userId)) + if (!data.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) return false; user = UserRepository.Get().FirstOrDefault(x => x.Id == userId); return user != null; - }, data => { data.Add("userId", user!.Id.ToString()); }); + }, data => { data.Add("userId", user!.Id); }); if (access == null) throw new HttpApiException("Unable to validate access", 400); @@ -115,6 +112,39 @@ public class OAuth2Controller : Controller return access; } + [HttpPost("refresh")] + public async Task Refresh( + [FromForm(Name = "grant_type")] string grantType, + [FromForm(Name = "refresh_token")] string refreshToken + ) + { + if (grantType != "refresh_token") + throw new HttpApiException("Invalid grant type", 400); + + var refreshData = await OAuth2Service.RefreshAccess(refreshToken, (refreshTokenData, newTokenData) => + { + // Check if the userId is present in the refresh token + if (!refreshTokenData.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) + return false; + + // Load user from database if existent + var user = UserRepository + .Get() + .FirstOrDefault(x => x.Id == userId); + + if (user == null) + return false; + + newTokenData.Add("userId", user.Id); + return true; + }); + + if(refreshData == null) + throw new HttpApiException("Unable to validate refresh", 400); + + return refreshData; + } + [HttpGet("info")] public async Task Info() { @@ -127,13 +157,13 @@ public class OAuth2Controller : Controller 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)) + if (!data.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) return false; currentUser = UserRepository @@ -149,8 +179,8 @@ public class OAuth2Controller : Controller if (!isValid) throw new HttpApiException("Invalid access token", 401); - - if(currentUser == null) + + if (currentUser == null) throw new HttpApiException("Invalid access token", 401); return new InfoResponse() diff --git a/Moonlight.ApiServer/Moonlight.ApiServer.csproj b/Moonlight.ApiServer/Moonlight.ApiServer.csproj index ca4c332c..3f3fe824 100644 --- a/Moonlight.ApiServer/Moonlight.ApiServer.csproj +++ b/Moonlight.ApiServer/Moonlight.ApiServer.csproj @@ -12,8 +12,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/Moonlight.ApiServer/Program.cs b/Moonlight.ApiServer/Program.cs index 6f3cf15a..2cd8d78d 100644 --- a/Moonlight.ApiServer/Program.cs +++ b/Moonlight.ApiServer/Program.cs @@ -87,7 +87,6 @@ builder.Services.AddSwaggerGen(); builder.Services.AddSingleton(configService); -builder.Services.AddSingleton(); builder.Services.AutoAddServices(); // OAuth2 @@ -141,7 +140,7 @@ builder.Services.AddTokenAuthentication(configuration => configuration.AccessSecret = config.Authentication.AccessSecret; configuration.DataLoader = async (data, provider, context) => { - if (!data.TryGetValue("userId", out var userIdStr) || !int.TryParse(userIdStr, out var userId)) + if (!data.TryGetValue("userId", out var userIdStr) || !userIdStr.TryGetInt32(out var userId)) return false; var userRepo = provider.GetRequiredService>(); @@ -211,7 +210,7 @@ app.UseRouting(); app.UseMiddleware(); -app.UseTokenAuthentication(_ => {}); +app.UseTokenAuthentication(); app.UseMiddleware(); diff --git a/Moonlight.ApiServer/Properties/launchSettings.json b/Moonlight.ApiServer/Properties/launchSettings.json index 6ca866e6..ccaf2211 100644 --- a/Moonlight.ApiServer/Properties/launchSettings.json +++ b/Moonlight.ApiServer/Properties/launchSettings.json @@ -4,11 +4,11 @@ "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "applicationUrl": "http://localhost:5165", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" - } + }, + "hotReloadEnabled": true } } } \ No newline at end of file diff --git a/Moonlight.ApiServer/Services/AuthService.cs b/Moonlight.ApiServer/Services/AuthService.cs index 5397727d..92538e59 100644 --- a/Moonlight.ApiServer/Services/AuthService.cs +++ b/Moonlight.ApiServer/Services/AuthService.cs @@ -13,16 +13,13 @@ public class AuthService { private readonly DatabaseRepository UserRepository; private readonly ConfigService ConfigService; - private readonly JwtHelper JwtHelper; public AuthService( DatabaseRepository userRepository, - ConfigService configService, - JwtHelper jwtHelper) + ConfigService configService) { UserRepository = userRepository; ConfigService = configService; - JwtHelper = jwtHelper; } public Task Register(string username, string email, string password) diff --git a/Moonlight.Client/Moonlight.Client.csproj b/Moonlight.Client/Moonlight.Client.csproj index af90e3bd..83c71079 100644 --- a/Moonlight.Client/Moonlight.Client.csproj +++ b/Moonlight.Client/Moonlight.Client.csproj @@ -10,7 +10,7 @@ - + diff --git a/Moonlight.Client/Program.cs b/Moonlight.Client/Program.cs index a9c4685f..87f09776 100644 --- a/Moonlight.Client/Program.cs +++ b/Moonlight.Client/Program.cs @@ -16,6 +16,7 @@ using Moonlight.Client.Interfaces; using Moonlight.Client.Services; using Moonlight.Client.UI; using Moonlight.Shared.Http.Requests.Auth; +using Moonlight.Shared.Http.Responses.Auth; // Build pre run logger var providers = LoggerBuildHelper.BuildFromConfiguration(configuration => @@ -70,13 +71,19 @@ builder.Services.AddScoped(sp => DateTimeOffset.FromUnixTimeSeconds(long.Parse(await cookieService.GetValue("kms-timestamp", "0"))).UtcDateTime, async refreshToken => { - await httpClient.PostAsync("api/auth/refresh", new StringContent( + var response = await httpClient.PostAsync("api/auth/refresh", new StringContent( JsonSerializer.Serialize(new RefreshRequest() { RefreshToken = refreshToken }), new MediaTypeHeaderValue("application/json") )); + var refreshRes = await response.ParseAsJson(); + + await cookieService.SetValue("kms-access", refreshRes.AccessToken, 10); + await cookieService.SetValue("kms-refresh", refreshRes.RefreshToken, 10); + await cookieService.SetValue("kms-timestamp", DateTimeOffset.UtcNow.AddSeconds(60).ToUnixTimeSeconds().ToString(), 10); + return new TokenPair() { AccessToken = await cookieService.GetValue("kms-access", "x"),