// SPDX-FileCopyrightText: 2026 Progmasoft // SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1 using OpenIddict.Abstractions; using Progmasoft.OAuth.Developers; using Progmasoft.Progmaweb.Api.Accounts; using static OpenIddict.Abstractions.OpenIddictConstants; namespace Progmasoft.OAuth.Api; /// What the consent page shows about an authorization request. /// The client identifier. /// The name of the application. /// The address of the application's site, or empty. /// The account that registered the application, or . /// The origin, or for a native application the scheme, that the account holder returns to. /// The requested scopes the consent page lists, openid left out. /// The account that is signed in. internal sealed record ConsentView( string ClientId, string DisplayName, string Homepage, string? Owner, string ReturnTo, IReadOnlyList Scopes, string AccountName); /// The data of the consent page. /// /// /connect/authorize sends an account holder who has not allowed the request yet to the consent page with /// the parameters of the request. The page reads what to show here and posts the decision back to /// /connect/authorize, where OpenIddict checks the whole request again. The checks here only keep the page /// from showing an application, an address or a scope that the request could not use. /// internal static class ConsentEndpoints { /// Maps the endpoint. /// The /api/v1/oauth group. public static void MapConsentEndpoints(this RouteGroupBuilder api) => api.MapGet("/consent", DescribeAsync); /// Describes an authorization request for the consent page. private static async Task DescribeAsync( HttpContext context, IOpenIddictApplicationManager applications, ApplicationRegistry registry, AccountService accounts, string? client_id, string? redirect_uri, string? scope) { if (await OAuthAccount.CurrentAsync(context) is not { } account) { return OAuthAccount.SignInRequired(); } object? application = string.IsNullOrEmpty(client_id) ? null : await applications.FindByClientIdAsync(client_id, context.RequestAborted); if (application is null || !await applications.HasPermissionAsync(application, Permissions.Endpoints.Authorization, context.RequestAborted) || string.IsNullOrEmpty(redirect_uri) || !Uri.TryCreate(redirect_uri, UriKind.Absolute, out Uri? redirect) || !await applications.ValidateRedirectUriAsync(application, redirect_uri, context.RequestAborted)) { return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: "This request does not name a registered application and address."); } List scopes = []; foreach (string requested in (scope ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries) .Distinct(StringComparer.Ordinal)) { if (requested == Scopes.OpenId) { continue; } if (OAuthScopes.Find(requested) is null || !await applications.HasPermissionAsync(application, Permissions.Prefixes.Scope + requested, context.RequestAborted)) { return Results.Problem(statusCode: StatusCodes.Status400BadRequest, title: "The application may not request this scope.", detail: requested); } scopes.Add(requested); } string applicationId = (await applications.GetIdAsync(application, context.RequestAborted))!; Guid? ownerId = await registry.FindOwnerAsync(applicationId, context.RequestAborted); AccountRecord? owner = ownerId is { } id ? await accounts.FindAsync(id, context.RequestAborted) : null; RegisteredApplication? registered = ownerId is { } ownerAccount ? await registry.FindOwnedAsync(ownerAccount, client_id!, context.RequestAborted) : null; string returnTo = redirect.Scheme is "http" or "https" ? redirect.GetLeftPart(UriPartial.Authority) : redirect.Scheme + ":"; return Results.Ok(new ConsentView( client_id!, await applications.GetDisplayNameAsync(application, context.RequestAborted) ?? client_id!, registered?.Homepage ?? string.Empty, owner?.AccountName, returnTo, scopes, account.AccountName)); } }