using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server.AspNetCore; using PrivaPub.StaticServices; using System.Security.Claims; using static OpenIddict.Abstractions.OpenIddictConstants; namespace PrivaPub.Api.Mastodon.Auth { [ApiController] public class TokenController : ControllerBase { readonly DbEntities _dbEntities; public TokenController(DbEntities dbEntities) { _dbEntities = dbEntities; } [HttpPost("/oauth/token"), IgnoreAntiforgeryToken, Produces("application/json")] public async Task Exchange(CancellationToken token) { var request = HttpContext.GetOpenIddictServerRequest(); if (request == default) return BadRequest(); if (request.IsAuthorizationCodeGrantType()) { var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; if (principal == default || !await Usable(principal.GetClaim(Claims.Subject), token)) return Refuse(Errors.InvalidGrant, "The persona can no longer be used."); return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } if (request.IsClientCredentialsGrantType()) { var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); identity.SetClaim(Claims.Subject, "app:" + request.ClientId); identity.SetScopes(MastodonScopes.Parse(request.Scope)); identity.SetDestinations(_ => new[] { Destinations.AccessToken }); return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } if (request.IsTokenExchangeGrantType()) return await Persona(request, token); return Refuse(Errors.UnsupportedGrantType, "The grant type is not supported."); } //PersonaExchange: the root JWT, validated by RootJwtSubjectToken, for a token of one of the root's personas async Task Persona(OpenIddictRequest request, CancellationToken token) { var subject = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal; var rootId = subject?.GetClaim(PersonaExchange.RootClaim); var avatarId = (string)request[PersonaExchange.AvatarParameter]; if (string.IsNullOrEmpty(rootId) || string.IsNullOrEmpty(avatarId)) return Refuse(Errors.InvalidGrant, "The persona can no longer be used."); var owns = await _dbEntities.RootToAvatars.Match(r => r.RootId == rootId && r.AvatarId == avatarId).ExecuteAnyAsync(token); var avatar = owns ? await _dbEntities.Avatars.MatchID(avatarId).ExecuteFirstAsync(token) : default; if (avatar == default || !await Usable(avatarId, token)) return Refuse(Errors.InvalidGrant, "The persona can no longer be used."); var identity = new ClaimsIdentity(TokenValidationParameters.DefaultAuthenticationType, Claims.Name, Claims.Role); identity.SetClaim(Claims.Subject, avatar.ID); identity.SetClaim(Claims.Name, avatar.UserName); identity.SetScopes(MastodonScopes.Parse(request.Scope).Intersect(new[] { "read", "write", "follow" })); identity.SetDestinations(_ => new[] { Destinations.AccessToken }); return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } async Task Usable(string avatarId, CancellationToken token) { if (string.IsNullOrEmpty(avatarId)) return false; var link = await _dbEntities.RootToAvatars.Match(r => r.AvatarId == avatarId).ExecuteFirstAsync(token); var root = link == default ? default : await _dbEntities.RootUsers.MatchID(link.RootId).ExecuteFirstAsync(token); var avatar = await _dbEntities.Avatars.MatchID(avatarId).ExecuteFirstAsync(token); return root is { IsBanned: false, DeletedAt: null } && avatar is { DeletionAt: null }; } ForbidResult Refuse(string error, string description) => Forbid(new AuthenticationProperties(new Dictionary { [OpenIddictServerAspNetCoreConstants.Properties.Error] = error, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = description }), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); } }