|
- using System.IdentityModel.Tokens.Jwt;
- using System.Security.Claims;
- using Campaign_Tracker.Server.Audit;
- using Campaign_Tracker.Server.Authentication;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
-
- namespace Campaign_Tracker.Server.Controllers;
-
- [ApiController]
- [AllowAnonymous]
- [Route("api/auth/token")]
- public sealed class AuthTokenController : ControllerBase
- {
- private readonly IHostEnvironment _environment;
- private readonly IAuditService _auditService;
- private readonly IKeycloakTokenClient _tokenClient;
-
- public AuthTokenController(
- IKeycloakTokenClient tokenClient,
- IAuditService auditService,
- IHostEnvironment environment)
- {
- _auditService = auditService;
- _environment = environment;
- _tokenClient = tokenClient;
- }
-
- [HttpPost("exchange")]
- public async Task<ActionResult<AuthTokenSetResponse>> Exchange(
- [FromBody] AuthorizationCodeExchangeRequest request,
- CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(request.Code) ||
- string.IsNullOrWhiteSpace(request.RedirectUri))
- {
- RecordTokenEvent(
- AuditEventType.SessionLoginFailure,
- "anonymous",
- "authentication/token/exchange",
- "invalid request");
- return BadRequest();
- }
-
- try
- {
- var tokens = await _tokenClient.ExchangeAuthorizationCodeAsync(
- request.Code,
- request.RedirectUri,
- cancellationToken);
- RecordTokenEvent(
- AuditEventType.SessionLogin,
- ExtractSubject(tokens.AccessToken),
- "authentication/token/exchange",
- "success");
-
- return Ok(tokens);
- }
- catch (KeycloakTokenRequestException exception)
- {
- RecordTokenEvent(
- AuditEventType.SessionLoginFailure,
- "anonymous",
- "authentication/token/exchange",
- $"keycloak rejected request: {(int)exception.StatusCode}");
- return Unauthorized(CreateTokenExchangeProblem(exception));
- }
- }
-
- [HttpPost("refresh")]
- public async Task<ActionResult<AuthTokenSetResponse>> Refresh(
- [FromBody] RefreshTokenRequest request,
- CancellationToken cancellationToken)
- {
- if (string.IsNullOrWhiteSpace(request.RefreshToken))
- {
- RecordTokenEvent(
- AuditEventType.SessionRefreshFailure,
- "anonymous",
- "authentication/token/refresh",
- "invalid request");
- return BadRequest();
- }
-
- try
- {
- var tokens = await _tokenClient.RefreshAccessTokenAsync(
- request.RefreshToken,
- cancellationToken);
- RecordTokenEvent(
- AuditEventType.SessionRefresh,
- ExtractSubject(tokens.AccessToken),
- "authentication/token/refresh",
- "success");
-
- return Ok(tokens);
- }
- catch (KeycloakTokenRequestException exception)
- {
- RecordTokenEvent(
- AuditEventType.SessionRefreshFailure,
- "anonymous",
- "authentication/token/refresh",
- $"keycloak rejected request: {(int)exception.StatusCode}");
- return Unauthorized(CreateTokenExchangeProblem(exception));
- }
- }
-
- private void RecordTokenEvent(
- string eventType,
- string actorIdentity,
- string resource,
- string outcome)
- {
- _auditService.Record(new AuditEvent(
- eventType,
- actorIdentity,
- resource,
- outcome,
- HttpContext.TraceIdentifier,
- DateTimeOffset.UtcNow));
- }
-
- private static string ExtractSubject(string accessToken)
- {
- try
- {
- var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
- return jwt.Claims.FirstOrDefault(claim =>
- claim.Type == JwtRegisteredClaimNames.Sub ||
- claim.Type == ClaimTypes.NameIdentifier ||
- claim.Type == ClaimTypes.Name ||
- claim.Type == "preferred_username")
- ?.Value ?? "unknown";
- }
- catch
- {
- return "unknown";
- }
- }
-
- private object CreateTokenExchangeProblem(KeycloakTokenRequestException exception)
- {
- if (!_environment.IsDevelopment())
- {
- return new { error = "Keycloak token request failed." };
- }
-
- return new
- {
- error = "Keycloak token request failed.",
- statusCode = (int)exception.StatusCode,
- keycloakResponse = exception.ResponseBody,
- };
- }
- }
-
- public sealed record AuthorizationCodeExchangeRequest(string Code, string RedirectUri);
-
- public sealed record RefreshTokenRequest(string RefreshToken);
|