using System.Net.Http.Headers; using System.Text.Json; using FrameworkBLL.KeyCloak; using FrameworkDAL.DTO.KeyCloak; using Microsoft.Extensions.Options; using FastEndpoints; namespace FrameworkSL.Endpoints.KeyCloak { // H-17 fix: migrated off MVC ControllerBase (was KeyCloakService.Logout under // [Route("Authorize")]) onto native FastEndpoints. public class KeyCloakLogout : EndpointWithoutRequest { private readonly KeycloakOptions _keycloakOptions; private readonly IHttpClientFactory _httpClientFactory; private readonly IKeyCloakBLL _keyCloakBLL; private readonly ILogger _logger; public KeyCloakLogout( IOptions keycloakOptions, IHttpClientFactory httpClientFactory, IKeyCloakBLL keyCloakBLL, ILogger logger) { _keycloakOptions = keycloakOptions.Value; _httpClientFactory = httpClientFactory; _keyCloakBLL = keyCloakBLL; _logger = logger; } public override void Configure() { Get("/Authorize/Logout"); AllowAnonymous(); } public override async Task HandleAsync(CancellationToken ct) { string idToken = HttpContext.Session.GetString("id_token")!; string clientId = HttpContext.Session.GetString("client_id")!; string realm = HttpContext.Session.GetString("realm")!; string keycloakHost = HttpContext.Session.GetString("keycloak_base_url")!; if (string.IsNullOrEmpty(idToken) || string.IsNullOrEmpty(clientId) || string.IsNullOrEmpty(realm) || string.IsNullOrEmpty(keycloakHost)) { await Send.StringAsync("Missing logout session data.", 400, cancellation: ct); return; } try { string masterToken = await _keyCloakBLL.GetMasterAccessToken(); string clientUuid = await GetClientUuid(masterToken, realm, clientId, ct); string postLogoutRedirectUri = await GetPostLogoutRedirectUri(masterToken, realm, clientUuid, ct); HttpContext.Session.Clear(); string logoutUrl = $"{keycloakHost}/realms/{realm}/protocol/openid-connect/logout" + $"?id_token_hint={Uri.EscapeDataString(idToken)}" + $"&post_logout_redirect_uri={Uri.EscapeDataString(postLogoutRedirectUri)}" + $"&client_id={Uri.EscapeDataString(clientId)}"; await Send.RedirectAsync(logoutUrl, allowRemoteRedirects: true); } catch (Exception ex) { _logger.LogError(ex, "KeyCloakLogout failed"); await Send.StringAsync("Logout failed. Please try again.", 500, cancellation: ct); } } private async Task GetClientUuid(string token, string realm, string clientId, CancellationToken ct) { var client = _httpClientFactory.CreateClient("oidc"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.GetAsync( $"{_keycloakOptions.KeycloakHost}/admin/realms/{realm}/clients?clientId={clientId}", ct); if (!response.IsSuccessStatusCode) { string error = await response.Content.ReadAsStringAsync(ct); throw new Exception($"Failed to fetch Keycloak client UUID. Status: {response.StatusCode}, Error: {error}"); } string content = await response.Content.ReadAsStringAsync(ct); JsonDocument json = JsonDocument.Parse(content); var clients = json.RootElement; if (clients.GetArrayLength() == 0) throw new Exception($"Client '{clientId}' not found in Keycloak realm '{realm}'."); return clients[0].GetProperty("id").GetString()!; } private async Task GetPostLogoutRedirectUri(string token, string realm, string clientUuid, CancellationToken ct) { var client = _httpClientFactory.CreateClient("oidc"); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); HttpResponseMessage response = await client.GetAsync( $"{_keycloakOptions.KeycloakHost}/admin/realms/{realm}/clients/{clientUuid}", ct); if (!response.IsSuccessStatusCode) { string error = await response.Content.ReadAsStringAsync(ct); throw new Exception($"Failed to get Keycloak client configuration. StatusCode: {response.StatusCode}, Error: {error}"); } string content = await response.Content.ReadAsStringAsync(ct); JsonDocument json = JsonDocument.Parse(content); if (json.RootElement.TryGetProperty("attributes", out JsonElement attributes) && attributes.TryGetProperty("post.logout.redirect.uris", out JsonElement postLogoutUris)) { string uris = postLogoutUris.GetString()!; if (!string.IsNullOrWhiteSpace(uris)) return uris.Split(',')[0].Trim(); } if (json.RootElement.TryGetProperty("redirectUris", out JsonElement redirectUris) && redirectUris.GetArrayLength() > 0) { return redirectUris[0].GetString()!; } throw new Exception("No post logout redirect URI found for the client."); } } }