Introduce a new authentication mechanism using JWT tokens stored in cookies, with a custom CookieAuthHandler for API request authentication. Add AuthServiceSettings for configuration and UserHeaderHandler to propagate user context in outgoing HTTP requests. Update service registrations and configuration files to support the new authentication flow. Refactor CurrentUserService for simplicity. This enables stateless, cookie-based authentication and consistent user context across API calls.
113 lines
4.1 KiB
C#
113 lines
4.1 KiB
C#
using Microsoft.AspNetCore.Authentication;
|
||
using Microsoft.Extensions.Options;
|
||
using System.Security.Claims;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
|
||
namespace DbFirst.API.Services;
|
||
|
||
/// <summary>
|
||
/// 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).
|
||
/// </summary>
|
||
public class CookieAuthHandler(
|
||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||
ILoggerFactory logger,
|
||
System.Text.Encodings.Web.UrlEncoder encoder)
|
||
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
|
||
{
|
||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||
{
|
||
var log = logger.CreateLogger<CookieAuthHandler>();
|
||
|
||
// 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));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Dekodiert den Payload-Teil eines JWT (Base64Url) und gibt den Wert
|
||
/// des angegebenen Claims zurück.
|
||
/// </summary>
|
||
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;
|
||
}
|
||
}
|