From cf6ce0d73657185d9111c504f0dcf63271c0c23b Mon Sep 17 00:00:00 2001 From: chris Date: Sat, 31 May 2025 02:16:10 +0200 Subject: [PATCH] Implement token caching and refresh mechanism for auth. Added distributed caching for access and refresh tokens in both `AuthController` and `AppProxyController`. Introduced logic to handle token expiration and refresh, ensuring continuous authorization through token renewal and session management. --- .../Controllers/AppProxyController.cs | 74 +++++++++++++-- .../Controllers/AuthController.cs | 91 +++++++++++++++---- 2 files changed, 136 insertions(+), 29 deletions(-) diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs index d315d66..62e2ec7 100644 --- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs +++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AppProxyController.cs @@ -1,9 +1,8 @@ using System.Net; using System.Net.Http.Headers; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Server.HttpSys; using Microsoft.Extensions.Caching.Distributed; +using Suspectus.Gandalf.Core.Abstractions.DTOs.Internal.Auth; using Suspectus.Gandalf.Core.Abstractions.Extensions; using Suspectus.Gandalf.Palantir.Client; @@ -49,6 +48,11 @@ public class AppProxyController : ControllerBase } var token = await GetToken(sessionCookie, appId, cancellationToken); + + if (token is null) + { + return Unauthorized("Session expired."); + } _httpClient.BaseAddress = new Uri(appInfo.BaseAddress); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); @@ -78,32 +82,82 @@ public class AppProxyController : ControllerBase return new EmptyResult(); } - private async Task GetToken(string subjectId, string appId, CancellationToken cancellationToken) + private async Task GetToken(string subjectId, string appId, CancellationToken cancellationToken) { - // TODO: Get actual token for subject and app var accessTokenCacheKey = GetCacheKey(subjectId, appId, "access_token"); var accessToken = await _tokenCache.GetStringAsync(accessTokenCacheKey, cancellationToken); - if (accessToken is not null) + if (accessToken is not null) + { return accessToken; + } + + var refreshTokenCacheKey = GetCacheKey(subjectId, appId, "refresh_token"); + var refreshToken = await _tokenCache.GetStringAsync(refreshTokenCacheKey, cancellationToken); + + if (refreshToken is null) + { + return null; + } + + var tokenRequestCommandResponse = await _client.Internal.Auth.Token(new TokenRequestCommand + { + GrandType = GrandType.RefreshToken, + RefreshToken = refreshToken, + ClientId = appId + }); + + if (tokenRequestCommandResponse.IsFaulted) + { + return null; + } + + var tokenRequestResponse = tokenRequestCommandResponse.GetValue(); + + if (tokenRequestResponse is null) + { + return null; + } - const string newToken = "totallyARealToken"; // Replace with actual token retrieval logic await _tokenCache.SetStringAsync( accessTokenCacheKey, - newToken, + tokenRequestResponse.AccessToken, new DistributedCacheEntryOptions { - AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(1), + AbsoluteExpiration = tokenRequestResponse.AccessTokenExpiresAt.AddSeconds(-10), }, cancellationToken ); - accessToken = newToken; - return accessToken; + var refreshTokenCacheExpiration = tokenRequestResponse.RefreshTokenExpiresAt.AddSeconds(-10); + + await _tokenCache.SetStringAsync( + refreshTokenCacheKey, + tokenRequestResponse.RefreshToken, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = refreshTokenCacheExpiration, + }, + cancellationToken + ); + + UpdateSessionCookie(subjectId, refreshTokenCacheExpiration); + return tokenRequestResponse.AccessToken; } private string GetCacheKey(string subjectId, string appId, string tokenType) { return $"{subjectId}:{appId}:{tokenType}"; } + + private void UpdateSessionCookie(string subjectId, DateTimeOffset refreshTokenCacheExpiration) + { + Response.Cookies.Append("MithrandirSession", subjectId, new CookieOptions + { + Secure = true, + HttpOnly = true, + SameSite = SameSiteMode.Lax, + Expires = refreshTokenCacheExpiration + }); + } } \ No newline at end of file diff --git a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs index ac40b6d..9e64c3e 100644 --- a/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs +++ b/src/dotnet/Suspectus.Gandalf.Bridgekeeper.Api/Controllers/AuthController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Caching.Distributed; using Suspectus.Gandalf.Core.Abstractions.CQRS.Commands; +using Suspectus.Gandalf.Core.Abstractions.DTOs.Internal.Auth; +using Suspectus.Gandalf.Core.Abstractions.Extensions; using Suspectus.Gandalf.Palantir.Client; -using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth; namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers; @@ -10,12 +12,14 @@ namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers; public class AuthController : ControllerBase { private readonly IPalantirClient _client; + private readonly IDistributedCache _tokenCache; - public AuthController(IPalantirClient client) + public AuthController(IPalantirClient client, IDistributedCache tokenCache) { _client = client; + _tokenCache = tokenCache; } - + [HttpGet("[action]")] public async Task Check() { @@ -23,28 +27,72 @@ public class AuthController : ControllerBase } [HttpPost("[action]")] - public async Task Login([FromBody] LoginCommand loginCommand) + public async Task Login([FromBody] ValidateCredentialsCommand validateCredentialsCommand, [FromQuery] string? clientId, CancellationToken cancellationToken) { - var validationResult = await _client.Internal.Auth.ValidateCredentials(new ValidateCredentialsCommand { Password = loginCommand.Password, UsernameOrEmail = loginCommand.UsernameOrEmail }); + if (clientId is null) + { + var masterAppInfoResponse = await _client.Internal.App.GetMasterInfo(); - return validationResult.Match(valid => + if (masterAppInfoResponse.IsFaulted) { - if (!valid) - { - return BadRequest("Invalid username or password."); - } + return BadRequest($"Failed to retrieve master app info."); + } - Response.Cookies.Append("MithrandirSession", loginCommand.UsernameOrEmail, new CookieOptions - { - Secure = true, - HttpOnly = true, - SameSite = SameSiteMode.Lax, - Expires = DateTime.UtcNow.AddMinutes(30) - }); + clientId = masterAppInfoResponse.GetValue().Id; + } - return Ok(); - }, e => BadRequest($"{e.Message}\n{e.InnerException?.Message}") + var tokenRequestCommandResponse = await _client.Internal.Auth.Token(new TokenRequestCommand + { + Password = validateCredentialsCommand.Password, + Username = validateCredentialsCommand.UsernameOrEmail, + GrandType = GrandType.Password, + ClientId = clientId + }); + + if (tokenRequestCommandResponse.IsFaulted) + { + return tokenRequestCommandResponse.Match( + BadRequest, + e => BadRequest($"{e.Message}\n{e.InnerException?.Message}") + ); + } + + var tokenRequestResponse = tokenRequestCommandResponse.GetValue(); + + if (tokenRequestResponse is null) + { + return BadRequest("Invalid username or password."); + } + + await _tokenCache.SetStringAsync( + GetCacheKey(tokenRequestResponse.SubjectId, clientId, "access_token"), + tokenRequestResponse.AccessToken, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = tokenRequestResponse.AccessTokenExpiresAt.AddSeconds(-10), + }, + cancellationToken ); + + await _tokenCache.SetStringAsync( + GetCacheKey(tokenRequestResponse.SubjectId, clientId, "refresh_token"), + tokenRequestResponse.RefreshToken, + new DistributedCacheEntryOptions + { + AbsoluteExpiration = tokenRequestResponse.RefreshTokenExpiresAt.AddSeconds(-10), + }, + cancellationToken + ); + + Response.Cookies.Append("MithrandirSession", tokenRequestResponse.SubjectId, new CookieOptions + { + Secure = true, + HttpOnly = true, + SameSite = SameSiteMode.Lax, + Expires = tokenRequestResponse.RefreshTokenExpiresAt.AddSeconds(-10) + }); + + return Ok(); } [HttpGet("[action]")] @@ -60,4 +108,9 @@ public class AuthController : ControllerBase { return Ok(true); } + + private string GetCacheKey(string subjectId, string appId, string tokenType) + { + return $"{subjectId}:{appId}:{tokenType}"; + } } \ No newline at end of file