using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using OpenIddict.Abstractions; using PrivaPub.Api.Mastodon.Auth; using PrivaPub.Api.Mastodon.Entities; using PrivaPub.Api.Mastodon.Infrastructure; using System.Security.Cryptography; using static OpenIddict.Abstractions.OpenIddictConstants; namespace PrivaPub.Api.Mastodon.Controllers { public class AppsController : MastodonController { readonly IOpenIddictApplicationManager _applications; public AppsController(IOpenIddictApplicationManager applications) { _applications = applications; } [HttpPost("/api/v1/apps"), AllowAnonymous] public async Task Create(CancellationToken token) { var name = Params.Get("client_name")?.Trim(); if (string.IsNullOrEmpty(name) || name.Length > 200) return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Application name can't be blank"); var redirects = Params.List("redirect_uris").SelectMany(r => r.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)).Distinct().ToList(); if (redirects.Count == 0 || redirects.Any(r => !Uri.TryCreate(r, UriKind.Absolute, out var uri) || uri.Scheme is "javascript" or "data")) return Error(StatusCodes.Status422UnprocessableEntity, "Validation failed: Redirect URI must be an absolute URI."); var scopes = MastodonScopes.Parse(string.Join(' ', Params.List("scopes"))); var descriptor = new OpenIddictApplicationDescriptor { ClientId = Secret(32), ClientSecret = Secret(32), ClientType = ClientTypes.Confidential, ConsentType = ConsentTypes.Explicit, DisplayName = name, Permissions = { Permissions.Endpoints.Authorization, Permissions.Endpoints.Token, Permissions.Endpoints.Revocation, Permissions.GrantTypes.AuthorizationCode, Permissions.GrantTypes.ClientCredentials, Permissions.ResponseTypes.Code } }; foreach (var scope in scopes) descriptor.Permissions.Add(Permissions.Prefixes.Scope + scope); foreach (var redirect in redirects) descriptor.RedirectUris.Add(new Uri(redirect)); var website = Params.Get("website"); if (Uri.TryCreate(website, UriKind.Absolute, out var site) && site.Scheme is "https" or "http") descriptor.Properties["website"] = System.Text.Json.JsonSerializer.SerializeToElement(website); var application = await _applications.CreateAsync(descriptor, token); return Json(new Application { Id = await _applications.GetIdAsync(application, token), Name = name, Website = website, Scopes = scopes.ToList(), RedirectUris = redirects, RedirectUri = string.Join("\n", redirects), ClientId = descriptor.ClientId, ClientSecret = descriptor.ClientSecret }); } [HttpGet("/api/v1/apps/verify_credentials")] public async Task Verify(CancellationToken token) { var clientId = User.GetPresenters().FirstOrDefault() ?? User.GetClaim(Claims.ClientId); var application = clientId == default ? default : await _applications.FindByClientIdAsync(clientId, token); if (application == default) return Error(StatusCodes.Status401Unauthorized, "The access token is invalid"); var properties = await _applications.GetPropertiesAsync(application, token); return Json(new { id = await _applications.GetIdAsync(application, token), name = await _applications.GetDisplayNameAsync(application, token), website = properties.TryGetValue("website", out var website) ? website.GetString() : default, scopes = User.GetScopes().ToList(), redirect_uris = (await _applications.GetRedirectUrisAsync(application, token)).ToList(), vapid_key = string.Empty }); } static string Secret(int bytes) => Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).TrimEnd('=').Replace('+', '-').Replace('/', '_'); } }