using Blazored.LocalStorage; using decePubClient.Models; using Microsoft.AspNetCore.Components.Authorization; using PrivaPub.ClientModels; using PrivaPub.ClientModels.User; using System.Security.Claims; namespace decePubClient.Services { /// /// The session (AuthData in localStorage, cached in memory) and the authentication state built from it. It only /// stores and hands out tokens; signing in, exchanging and revoking them is AuthService's job. /// public class TokenAuthStateProvider(ILocalStorageService storage, IStorage dbStorage, ILogger logger) : AuthenticationStateProvider { /// The claim naming the persona the client acts as (an avatar id). public const string PersonaClaim = "persona"; AuthData authData; bool loaded; /// The stored session, or an empty one. public async Task GetAuthData(CancellationToken cancellationToken = default) { if (loaded) return authData ?? new(); authData = await storage.GetItemAsync(nameof(AuthData), cancellationToken); loaded = true; return authData ?? new(); } /// The root's JWT while it is still valid, otherwise null. public async Task GetToken(CancellationToken cancellationToken = default) { var session = await GetAuthData(cancellationToken); if (string.IsNullOrEmpty(session.Token) || session.TokenExpiration is not { } expiration || expiration <= DateTime.UtcNow.Ticks) return null; return session.Token; } /// The current persona's Mastodon token, or null when the persona has none yet. public async Task GetPersonaToken(CancellationToken cancellationToken = default) { var session = await GetAuthData(cancellationToken); if (session.PersonaId is null) return null; return session.PersonaTokens.GetValueOrDefault(session.PersonaId); } public async Task IsAuthenticatedAsync(CancellationToken cancellationToken = default) => await GetToken(cancellationToken) is not null; /// Starts or renews the session with a JWT from login or sniff/again; persona tokens are kept. public async Task SetSession(JwtUser jwtUser, CancellationToken cancellationToken = default) { var session = await GetAuthData(cancellationToken); session.Token = jwtUser.Token; session.TokenExpiration = jwtUser.Expiration; session.TokenIssuedAt = DateTime.UtcNow.Ticks; session.UserName = jwtUser.Username; session.Policies = jwtUser.Policies ?? []; if (jwtUser.UserSettings?.LanguageCode is { Length: > 0 } language) session.CurrentLanguageCode = language; await Save(session, cancellationToken); } /// Makes the persona current, remembering its token when one is given. public async Task SetPersona(string personaId, string token = default, CancellationToken cancellationToken = default) { var session = await GetAuthData(cancellationToken); session.PersonaId = personaId; if (token is not null) session.PersonaTokens[personaId] = token; await Save(session, cancellationToken); } /// Drops a persona's token that PrivaPub no longer accepts. public async Task ForgetPersonaToken(string personaId, CancellationToken cancellationToken = default) { var session = await GetAuthData(cancellationToken); if (!session.PersonaTokens.Remove(personaId)) return; await Save(session, cancellationToken); } public async Task LogoutAsync(bool deleteDb = false, CancellationToken cancellationToken = default) { logger.LogInformation($"set null({nameof(LogoutAsync)})"); authData = null; loaded = true; await storage.RemoveItemAsync(nameof(AuthData), cancellationToken); if (deleteDb) await dbStorage.RemoveAll(includeClientLogs: true, cancellationToken: cancellationToken); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public override async Task GetAuthenticationStateAsync() { var token = await GetToken(); if (token is null) return new(new()); var session = await GetAuthData(); var claims = new List { new(ClaimTypes.Name, session.UserName ?? string.Empty) }; foreach (var policy in new[] { Policies.IsUser, Policies.IsModerator, Policies.IsAdmin }) claims.Add(new(policy, session.Policies.Contains(policy) ? "true" : "false")); if (session.PersonaId is not null) claims.Add(new(PersonaClaim, session.PersonaId)); return new(new(new ClaimsIdentity(claims, "jwt"))); } async Task Save(AuthData session, CancellationToken cancellationToken) { authData = session; loaded = true; await storage.SetItemAsync(nameof(AuthData), session, cancellationToken); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } } }