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.
This commit is contained in:
Christian Werner 2025-05-31 02:16:10 +02:00
parent 4ffc2a135c
commit cf6ce0d736
2 changed files with 136 additions and 29 deletions

View File

@ -1,9 +1,8 @@
using System.Net; using System.Net;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Server.HttpSys;
using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Distributed;
using Suspectus.Gandalf.Core.Abstractions.DTOs.Internal.Auth;
using Suspectus.Gandalf.Core.Abstractions.Extensions; using Suspectus.Gandalf.Core.Abstractions.Extensions;
using Suspectus.Gandalf.Palantir.Client; using Suspectus.Gandalf.Palantir.Client;
@ -49,6 +48,11 @@ public class AppProxyController : ControllerBase
} }
var token = await GetToken(sessionCookie, appId, cancellationToken); var token = await GetToken(sessionCookie, appId, cancellationToken);
if (token is null)
{
return Unauthorized("Session expired.");
}
_httpClient.BaseAddress = new Uri(appInfo.BaseAddress); _httpClient.BaseAddress = new Uri(appInfo.BaseAddress);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
@ -78,32 +82,82 @@ public class AppProxyController : ControllerBase
return new EmptyResult(); return new EmptyResult();
} }
private async Task<string> GetToken(string subjectId, string appId, CancellationToken cancellationToken) private async Task<string?> GetToken(string subjectId, string appId, CancellationToken cancellationToken)
{ {
// TODO: Get actual token for subject and app
var accessTokenCacheKey = GetCacheKey(subjectId, appId, "access_token"); var accessTokenCacheKey = GetCacheKey(subjectId, appId, "access_token");
var accessToken = await _tokenCache.GetStringAsync(accessTokenCacheKey, cancellationToken); var accessToken = await _tokenCache.GetStringAsync(accessTokenCacheKey, cancellationToken);
if (accessToken is not null) if (accessToken is not null)
{
return accessToken; 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( await _tokenCache.SetStringAsync(
accessTokenCacheKey, accessTokenCacheKey,
newToken, tokenRequestResponse.AccessToken,
new DistributedCacheEntryOptions new DistributedCacheEntryOptions
{ {
AbsoluteExpiration = DateTimeOffset.UtcNow.AddMinutes(1), AbsoluteExpiration = tokenRequestResponse.AccessTokenExpiresAt.AddSeconds(-10),
}, },
cancellationToken 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) private string GetCacheKey(string subjectId, string appId, string tokenType)
{ {
return $"{subjectId}:{appId}:{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
});
}
} }

View File

@ -1,7 +1,9 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using Suspectus.Gandalf.Core.Abstractions.CQRS.Commands; 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.Client;
using Suspectus.Gandalf.Palantir.Contracts.Controller.Auth;
namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers; namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
@ -10,12 +12,14 @@ namespace Suspectus.Gandalf.Bridgekeeper.Api.Controllers;
public class AuthController : ControllerBase public class AuthController : ControllerBase
{ {
private readonly IPalantirClient _client; private readonly IPalantirClient _client;
private readonly IDistributedCache _tokenCache;
public AuthController(IPalantirClient client) public AuthController(IPalantirClient client, IDistributedCache tokenCache)
{ {
_client = client; _client = client;
_tokenCache = tokenCache;
} }
[HttpGet("[action]")] [HttpGet("[action]")]
public async Task<IActionResult> Check() public async Task<IActionResult> Check()
{ {
@ -23,28 +27,72 @@ public class AuthController : ControllerBase
} }
[HttpPost("[action]")] [HttpPost("[action]")]
public async Task<IActionResult> Login([FromBody] LoginCommand loginCommand) public async Task<IActionResult> 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<IActionResult>(valid => if (masterAppInfoResponse.IsFaulted)
{ {
if (!valid) return BadRequest($"Failed to retrieve master app info.");
{ }
return BadRequest("Invalid username or password.");
}
Response.Cookies.Append("MithrandirSession", loginCommand.UsernameOrEmail, new CookieOptions clientId = masterAppInfoResponse.GetValue().Id;
{ }
Secure = true,
HttpOnly = true,
SameSite = SameSiteMode.Lax,
Expires = DateTime.UtcNow.AddMinutes(30)
});
return Ok(); var tokenRequestCommandResponse = await _client.Internal.Auth.Token(new TokenRequestCommand
}, e => BadRequest($"{e.Message}\n{e.InnerException?.Message}") {
Password = validateCredentialsCommand.Password,
Username = validateCredentialsCommand.UsernameOrEmail,
GrandType = GrandType.Password,
ClientId = clientId
});
if (tokenRequestCommandResponse.IsFaulted)
{
return tokenRequestCommandResponse.Match<IActionResult>(
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]")] [HttpGet("[action]")]
@ -60,4 +108,9 @@ public class AuthController : ControllerBase
{ {
return Ok(true); return Ok(true);
} }
private string GetCacheKey(string subjectId, string appId, string tokenType)
{
return $"{subjectId}:{appId}:{tokenType}";
}
} }