|
- using Campaign_Tracker.Server.Authentication;
- using System.Security.Claims;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
-
- namespace Campaign_Tracker.Server.Controllers;
-
- [ApiController]
- [Authorize]
- [Route("api/auth/logout")]
- public sealed class AuthLogoutController : ControllerBase
- {
- private readonly IKeycloakTokenClient _tokenClient;
- private readonly IAuthenticationAuditStore _auditStore;
- private readonly ILogger<AuthLogoutController> _logger;
-
- public AuthLogoutController(
- IKeycloakTokenClient tokenClient,
- IAuthenticationAuditStore auditStore,
- ILogger<AuthLogoutController> logger)
- {
- _tokenClient = tokenClient;
- _auditStore = auditStore;
- _logger = logger;
- }
-
- [HttpPost]
- public async Task<IActionResult> Logout(
- [FromBody] LogoutRequest? request,
- CancellationToken cancellationToken)
- {
- if (request is null || string.IsNullOrWhiteSpace(request.IdTokenHint))
- {
- return BadRequest();
- }
-
- var subject = User.Identity?.Name
- ?? User.FindFirstValue(ClaimTypes.NameIdentifier)
- ?? "unknown";
- var succeeded = false;
-
- try
- {
- await _tokenClient.EndSessionAsync(request.IdTokenHint, cancellationToken);
- succeeded = true;
- }
- catch (Exception ex)
- {
- _logger.LogWarning(
- ex,
- "Keycloak end-session call failed for subject {Subject}; client tokens will still be cleared.",
- subject);
- }
-
- _auditStore.RecordLogout(subject, succeeded, HttpContext.TraceIdentifier);
-
- // Always return 200 — per AC #8, client tokens must be cleared regardless of Keycloak availability.
- return Ok();
- }
- }
-
- public sealed record LogoutRequest(string IdTokenHint);
|