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