diff --git a/src/DfE.CoreLibs.Security/Authorization/ApiOboTokenService.cs b/src/DfE.CoreLibs.Security/Authorization/ApiOboTokenService.cs
index 1ca378b..8bc9cfc 100644
--- a/src/DfE.CoreLibs.Security/Authorization/ApiOboTokenService.cs
+++ b/src/DfE.CoreLibs.Security/Authorization/ApiOboTokenService.cs
@@ -2,73 +2,104 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
using Microsoft.Identity.Web;
using System.Security.Claims;
+using DfE.CoreLibs.Security.Configurations;
namespace DfE.CoreLibs.Security.Authorization
{
+
///
- /// Provides functionality for acquiring API On-Behalf-Of (OBO) tokens for authenticated users.
+ /// Provides functionality for acquiring API On-Behalf-Of (OBO) tokens for authenticated users with caching.
///
- public class ApiOboTokenService : IApiOboTokenService
+ public class ApiOboTokenService(
+ ITokenAcquisition tokenAcquisition,
+ IHttpContextAccessor httpContextAccessor,
+ IConfiguration configuration,
+ IMemoryCache memoryCache,
+ IOptions tokenSettingsOptions)
+ : IApiOboTokenService
{
- private readonly ITokenAcquisition _tokenAcquisition;
- private readonly IHttpContextAccessor _httpContextAccessor;
- private readonly IConfiguration _configuration;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The token acquisition service for acquiring tokens.
- /// Accessor for the current HTTP context, used to retrieve the user's claims.
- /// Configuration used to retrieve role-to-scope mappings.
- public ApiOboTokenService(
- ITokenAcquisition tokenAcquisition,
- IHttpContextAccessor httpContextAccessor,
- IConfiguration configuration)
- {
- _tokenAcquisition = tokenAcquisition;
- _httpContextAccessor = httpContextAccessor;
- _configuration = configuration;
- }
+ private readonly TokenSettings _tokenSettings = tokenSettingsOptions.Value;
///
public async Task GetApiOboTokenAsync(string? authenticationScheme = null)
{
- var userRoles = _httpContextAccessor.HttpContext?.User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray();
+ var user = httpContextAccessor.HttpContext?.User;
+ if (user == null)
+ {
+ throw new UnauthorizedAccessException("User is not authenticated.");
+ }
+
+ var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ if (string.IsNullOrEmpty(userId))
+ {
+ throw new InvalidOperationException("User ID is missing.");
+ }
+
+ // Retrieve user roles
+ var userRoles = user.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray();
if (userRoles == null || !userRoles.Any())
{
throw new UnauthorizedAccessException("User does not have any roles assigned.");
}
- var apiClientId = _configuration["Authorization:ApiSettings:ApiClientId"];
+ // Retrieve API client ID from configuration
+ var apiClientId = configuration["Authorization:ApiSettings:ApiClientId"];
if (string.IsNullOrWhiteSpace(apiClientId))
{
throw new InvalidOperationException("API client ID is missing from configuration.");
}
- var scopeMappings = _configuration.GetSection("Authorization:ScopeMappings").Get>>();
+ // Retrieve scope mappings from configuration
+ var scopeMappings = configuration.GetSection("Authorization:ScopeMappings").Get>>();
if (scopeMappings == null)
{
throw new InvalidOperationException("ScopeMappings section is missing from configuration.");
}
- // Map roles to scopes based on configuration, or use default scope if no roles match
- var apiScopes = userRoles.SelectMany(role => scopeMappings.ContainsKey(role) ? scopeMappings[role] : new List())
+ // Map roles to scopes based on configuration
+ var apiScopes = userRoles
+ .SelectMany(role => scopeMappings.TryGetValue(role, out var mapping) ? mapping : new List())
.Distinct()
- .Select(scope => $"api://{apiClientId}/{scope}")
- .ToArray();
+ .ToList();
if (!apiScopes.Any())
{
- var defaultScope = _configuration["ApiSettings:DefaultScope"];
- apiScopes = new[] { $"api://{apiClientId}/{defaultScope}" };
+ var defaultScope = configuration["Authorization:ApiSettings:DefaultScope"];
+ apiScopes = [defaultScope!];
}
- // Acquire the access token with the determined API scopes
- var apiToken = await _tokenAcquisition.GetAccessTokenForUserAsync(apiScopes, user: _httpContextAccessor.HttpContext?.User, authenticationScheme: authenticationScheme);
+ // Sort scopes to ensure consistent cache key generation
+ apiScopes.Sort(StringComparer.OrdinalIgnoreCase);
+ var scopesString = string.Join(",", apiScopes);
+
+ // Generate a unique cache key based on user ID and scopes
+ var cacheKey = $"ApiOboToken_{userId}_{scopesString}";
+
+ if (memoryCache.TryGetValue(cacheKey, out var cachedToken))
+ {
+ return cachedToken!;
+ }
+
+ // Acquire a new token
+ var formattedScopes = apiScopes.Select(scope => $"api://{apiClientId}/{scope}").ToArray();
+
+ var apiToken = await tokenAcquisition.GetAccessTokenForUserAsync(
+ formattedScopes,
+ user: user,
+ authenticationScheme: authenticationScheme);
+
+ // Calculate absolute expiration time: Now + Expiration - Buffer
+ var absoluteExpiration = DateTimeOffset.UtcNow
+ .AddMinutes(_tokenSettings.TokenLifetimeMinutes)
+ .Subtract(TimeSpan.FromSeconds(_tokenSettings.BufferInSeconds));
+
+ // Cache the token with absolute expiration
+ memoryCache.Set(cacheKey, apiToken, absoluteExpiration);
return apiToken;
}
}
-}
+}
\ No newline at end of file
diff --git a/src/DfE.CoreLibs.Security/Configurations/TokenSettings.cs b/src/DfE.CoreLibs.Security/Configurations/TokenSettings.cs
index b097371..f6b2aab 100644
--- a/src/DfE.CoreLibs.Security/Configurations/TokenSettings.cs
+++ b/src/DfE.CoreLibs.Security/Configurations/TokenSettings.cs
@@ -6,5 +6,6 @@ public class TokenSettings
public string Issuer { get; set; } = string.Empty;
public string Audience { get; set; } = string.Empty;
public int TokenLifetimeMinutes { get; set; } = 10;
+ public int BufferInSeconds { get; set; } = 60;
}
-}
+}
\ No newline at end of file