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;
}
}