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 _logger; public AuthLogoutController( IKeycloakTokenClient tokenClient, IAuthenticationAuditStore auditStore, ILogger logger) { _tokenClient = tokenClient; _auditStore = auditStore; _logger = logger; } [HttpPost] public async Task 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);