using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; using System.Security.Claims; using System.Text; using System.Text.Json; namespace DbFirst.API.Services; /// /// Authentifiziert eingehende API-Requests anhand des JWT-Tokens im Cookie. /// Das Token wird lokal dekodiert – ohne Rückruf zum Auth-Service – da es /// self-contained ist (Claim "unique_name" enthält den Benutzernamen). /// public class CookieAuthHandler( IOptionsMonitor options, ILoggerFactory logger, System.Text.Encodings.Web.UrlEncoder encoder) : AuthenticationHandler(options, logger, encoder) { protected override Task HandleAuthenticateAsync() { var log = logger.CreateLogger(); // 1. Cookie-Header lesen var cookieHeader = Request.Headers.Cookie.ToString(); if (string.IsNullOrEmpty(cookieHeader)) { log.LogDebug("CookieAuthHandler: Kein Cookie-Header vorhanden."); return Task.FromResult(AuthenticateResult.Fail("Kein Cookie vorhanden.")); } // 2. JWT aus dem "AuthToken"-Cookie extrahieren und Benutzernamen dekodieren var userName = TryExtractUserNameFromCookieJwt(cookieHeader); // 3. Fallback: X-Authenticated-User-Header if (string.IsNullOrEmpty(userName)) userName = Request.Headers["X-Authenticated-User"].ToString(); if (string.IsNullOrEmpty(userName)) { log.LogDebug("CookieAuthHandler: Kein Benutzername aus Token oder Header ermittelbar."); return Task.FromResult(AuthenticateResult.Fail("Kein Benutzername ermittelbar.")); } log.LogDebug("CookieAuthHandler: Authentifizierung erfolgreich für '{User}'.", userName); var claims = new[] { new Claim(ClaimTypes.Name, userName) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return Task.FromResult(AuthenticateResult.Success(ticket)); } /// /// Liest den "AuthToken"-Wert aus dem Cookie-Header und extrahiert /// den "unique_name"-Claim aus dem JWT-Payload (Base64Url-dekodiert). /// Keine Signaturprüfung – das Token wurde bereits beim Login/Restore /// durch den Auth-Service validiert. /// private static string? TryExtractUserNameFromCookieJwt(string cookieHeader) { foreach (var segment in cookieHeader.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)) { var eqIdx = segment.IndexOf('='); if (eqIdx < 0) continue; var name = segment[..eqIdx].Trim(); if (!name.Equals("AuthToken", StringComparison.OrdinalIgnoreCase)) continue; var token = segment[(eqIdx + 1)..].Trim(); return DecodeJwtClaim(token, "unique_name"); } return null; } /// /// Dekodiert den Payload-Teil eines JWT (Base64Url) und gibt den Wert /// des angegebenen Claims zurück. /// private static string? DecodeJwtClaim(string jwt, string claimName) { try { var parts = jwt.Split('.'); if (parts.Length != 3) return null; // Base64Url → Base64 → bytes → UTF-8 string var base64 = parts[1].Replace('-', '+').Replace('_', '/'); base64 = (base64.Length % 4) switch { 2 => base64 + "==", 3 => base64 + "=", _ => base64 }; var payloadJson = Encoding.UTF8.GetString(Convert.FromBase64String(base64)); using var doc = JsonDocument.Parse(payloadJson); if (doc.RootElement.TryGetProperty(claimName, out var prop)) return prop.GetString(); } catch { // Fehlerhafte Token stillschweigend ignorieren } return null; } }