2022-02-14 01:51:37 +01:00
|
|
|
using Blazored.LocalStorage;
|
2026-10-04 11:03:14 +02:00
|
|
|
|
2022-02-14 01:51:37 +01:00
|
|
|
using decePubClient.Models;
|
2026-10-04 11:03:14 +02:00
|
|
|
|
2022-02-14 01:51:37 +01:00
|
|
|
using Microsoft.AspNetCore.Components.Authorization;
|
2026-10-04 11:03:14 +02:00
|
|
|
|
|
|
|
|
using PrivaPub.ClientModels;
|
|
|
|
|
using PrivaPub.ClientModels.User;
|
|
|
|
|
|
2022-02-14 01:51:37 +01:00
|
|
|
using System.Security.Claims;
|
|
|
|
|
|
|
|
|
|
namespace decePubClient.Services
|
|
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
/// <summary>
|
|
|
|
|
/// 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.
|
|
|
|
|
/// </summary>
|
2026-10-03 15:26:21 +02:00
|
|
|
public class TokenAuthStateProvider(ILocalStorageService storage, IStorage dbStorage, ILogger<TokenAuthStateProvider> logger)
|
|
|
|
|
: AuthenticationStateProvider
|
2022-02-14 01:51:37 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
/// <summary>The claim naming the persona the client acts as (an avatar id).</summary>
|
|
|
|
|
public const string PersonaClaim = "persona";
|
|
|
|
|
|
|
|
|
|
AuthData authData;
|
|
|
|
|
bool loaded;
|
|
|
|
|
|
|
|
|
|
/// <summary>The stored session, or an empty one.</summary>
|
|
|
|
|
public async Task<AuthData> GetAuthData(CancellationToken cancellationToken = default)
|
2022-02-14 01:51:37 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
if (loaded)
|
|
|
|
|
return authData ?? new();
|
2022-02-14 01:51:37 +01:00
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
authData = await storage.GetItemAsync<AuthData>(nameof(AuthData), cancellationToken);
|
|
|
|
|
loaded = true;
|
|
|
|
|
return authData ?? new();
|
2022-02-14 01:51:37 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
/// <summary>The root's JWT while it is still valid, otherwise null.</summary>
|
2026-10-03 15:26:21 +02:00
|
|
|
public async Task<string> GetToken(CancellationToken cancellationToken = default)
|
2022-02-14 01:51:37 +01:00
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
var session = await GetAuthData(cancellationToken);
|
|
|
|
|
if (string.IsNullOrEmpty(session.Token) || session.TokenExpiration is not { } expiration || expiration <= DateTime.UtcNow.Ticks)
|
2026-10-03 15:26:21 +02:00
|
|
|
return null;
|
2026-10-04 11:03:14 +02:00
|
|
|
return session.Token;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>The current persona's Mastodon token, or null when the persona has none yet.</summary>
|
|
|
|
|
public async Task<string> GetPersonaToken(CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var session = await GetAuthData(cancellationToken);
|
|
|
|
|
if (session.PersonaId is null)
|
|
|
|
|
return null;
|
|
|
|
|
return session.PersonaTokens.GetValueOrDefault(session.PersonaId);
|
2022-02-14 01:51:37 +01:00
|
|
|
}
|
|
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
public async Task<bool> IsAuthenticatedAsync(CancellationToken cancellationToken = default) =>
|
|
|
|
|
await GetToken(cancellationToken) is not null;
|
|
|
|
|
|
2026-10-04 11:03:14 +02:00
|
|
|
/// <summary>Starts or renews the session with a JWT from login or sniff/again; persona tokens are kept.</summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Makes the persona current, remembering its token when one is given.</summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>Drops a persona's token that PrivaPub no longer accepts.</summary>
|
|
|
|
|
public async Task ForgetPersonaToken(string personaId, CancellationToken cancellationToken = default)
|
|
|
|
|
{
|
|
|
|
|
var session = await GetAuthData(cancellationToken);
|
|
|
|
|
if (!session.PersonaTokens.Remove(personaId))
|
|
|
|
|
return;
|
|
|
|
|
await Save(session, cancellationToken);
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-03 15:26:21 +02:00
|
|
|
public async Task LogoutAsync(bool deleteDb = false, CancellationToken cancellationToken = default)
|
2022-02-14 01:51:37 +01:00
|
|
|
{
|
2026-10-03 15:26:21 +02:00
|
|
|
logger.LogInformation($"set null({nameof(LogoutAsync)})");
|
2026-10-04 11:03:14 +02:00
|
|
|
authData = null;
|
|
|
|
|
loaded = true;
|
2026-10-03 15:26:21 +02:00
|
|
|
await storage.RemoveItemAsync(nameof(AuthData), cancellationToken);
|
2022-02-14 01:51:37 +01:00
|
|
|
if (deleteDb)
|
2026-10-03 15:26:21 +02:00
|
|
|
await dbStorage.RemoveAll(includeClientLogs: true, cancellationToken: cancellationToken);
|
2022-02-14 01:51:37 +01:00
|
|
|
|
|
|
|
|
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
|
|
|
|
|
{
|
2026-10-04 11:03:14 +02:00
|
|
|
var token = await GetToken();
|
|
|
|
|
if (token is null)
|
2022-02-14 01:51:37 +01:00
|
|
|
return new(new());
|
2026-10-04 11:03:14 +02:00
|
|
|
|
|
|
|
|
var session = await GetAuthData();
|
|
|
|
|
var claims = new List<Claim> { 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());
|
2022-02-14 01:51:37 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|