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.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<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 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
});
}
}

View File

@ -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<IActionResult> Check()
{
@ -23,28 +27,72 @@ public class AuthController : ControllerBase
}
[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("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<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]")]
@ -60,4 +108,9 @@ public class AuthController : ControllerBase
{
return Ok(true);
}
private string GetCacheKey(string subjectId, string appId, string tokenType)
{
return $"{subjectId}:{appId}:{tokenType}";
}
}