using System.Security.Claims; using System.Text.Encodings.Web; using Flaro.Core.Auth; using Flaro.Data; using Microsoft.AspNetCore.Authentication; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; namespace Flaro.Api.Auth; /// Weist Anfragen über ein Geräte-Token aus. /// /// /// /// Authorization: Bearer <token>. Der Server hasht das Vorgezeigte /// und sucht den Fingerabdruck — das Token selbst steht nirgends in der /// Datenbank und ist aus ihr auch nicht zu gewinnen. /// /// /// Keine JWT. Ein Token, das der Server nachschlägt, lässt sich sofort /// widerrufen; ein signiertes gilt bis zum Ablauf weiter, und für ein /// verlorenes Telefon ist das der falsche Satz. /// /// public sealed class TokenAuthenticationHandler( IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, FlaroContext db, TimeProvider clock) : AuthenticationHandler(options, logger, encoder) { public const string SchemeName = "FlaroToken"; public const string DeviceTokenIdClaim = "flaro:device"; protected override async Task HandleAuthenticateAsync() { if (!Request.Headers.TryGetValue("Authorization", out var kopfzeile)) return AuthenticateResult.NoResult(); var roh = kopfzeile.ToString(); const string vorsatz = "Bearer "; if (!roh.StartsWith(vorsatz, StringComparison.OrdinalIgnoreCase)) return AuthenticateResult.NoResult(); var token = roh[vorsatz.Length..].Trim(); if (token.Length == 0) return AuthenticateResult.NoResult(); var fingerabdruck = Secrets.Fingerprint(token); var eintrag = await db.DeviceTokens.FirstOrDefaultAsync(t => t.TokenFingerprint == fingerabdruck); var jetzt = clock.GetUtcNow(); if (eintrag is null || !eintrag.IsValid(jetzt)) return AuthenticateResult.Fail("Token gilt nicht."); var benutzer = await db.Users.FindAsync(eintrag.UserId); if (benutzer is null) return AuthenticateResult.Fail("Token gilt nicht."); // Für die Geräteliste: „zuletzt gesehen". Höchstens einmal je Stunde // geschrieben — bei jeder Anfrage zu schreiben hiesse, jede Anfrage in // einen Schreibzugriff zu verwandeln, und SQLite serialisiert die. if (jetzt - eintrag.LastSeenAt > TimeSpan.FromHours(1)) { eintrag.LastSeenAt = jetzt; await db.SaveChangesAsync(); } var ausweis = new ClaimsIdentity( [ new Claim(ClaimTypes.NameIdentifier, benutzer.Id.ToString()), new Claim(ClaimTypes.Name, benutzer.UserName), new Claim(DeviceTokenIdClaim, eintrag.Id.ToString()), .. benutzer.IsAdministrator ? new[] { new Claim(ClaimTypes.Role, FlaroRoles.Administrator) } : [], ], SchemeName); return AuthenticateResult.Success(new AuthenticationTicket(new ClaimsPrincipal(ausweis), SchemeName)); } } public static class FlaroRoles { public const string Administrator = "administrator"; }