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:
parent
4ffc2a135c
commit
cf6ce0d736
@ -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;
|
||||||
|
|
||||||
@ -50,6 +49,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
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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,10 +12,12 @@ 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]")]
|
||||||
@ -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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user