Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/apps/api/OAuth/Api/AllowedApplicationEndpoints.cs
blob: 03cdeba509d45a20a6c5d83b7de48739792e7b18 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
// SPDX-License-Identifier: AGPL-3.0-or-later WITH AdditionRef-Progmasoft-Patent-Grant-1.1

using System.Collections.Immutable;
using OpenIddict.Abstractions;
using static OpenIddict.Abstractions.OpenIddictConstants;

namespace Progmasoft.OAuth.Api;

/// <summary>An application the account has allowed.</summary>
/// <param name="AuthorizationId">The identifier of the allowance, used to withdraw it.</param>
/// <param name="DisplayName">The name of the application, or <see langword="null"/> when it was deleted.</param>
/// <param name="Scopes">The scopes the account allowed.</param>
/// <param name="CreatedAt">When the account allowed it.</param>
internal sealed record AllowedApplicationView(
    string AuthorizationId,
    string? DisplayName,
    IReadOnlyList<string> Scopes,
    DateTimeOffset? CreatedAt);

/// <summary>The applications an account has allowed, and withdrawing an allowance.</summary>
internal static class AllowedApplicationEndpoints
{
    /// <summary>Maps the endpoints.</summary>
    /// <param name="api">The <c>/api/v1/oauth</c> group.</param>
    public static void MapAllowedApplicationEndpoints(this RouteGroupBuilder api)
    {
        api.MapGet("/allowed", ListAsync);
        api.MapDelete("/allowed/{authorizationId}", RevokeAsync).RequireRateLimiting("oauth");
    }

    /// <summary>Lists the applications the signed-in account has allowed.</summary>
    private static async Task<IResult> ListAsync(
        HttpContext context,
        IOpenIddictAuthorizationManager authorizations,
        IOpenIddictApplicationManager applications)
    {
        if (await OAuthAccount.CurrentAsync(context) is not { } account)
        {
            return OAuthAccount.SignInRequired();
        }

        List<AllowedApplicationView> allowed = [];
        await foreach (object authorization in authorizations.FindBySubjectAsync(account.Id.ToString(),
                           context.RequestAborted))
        {
            if (!await authorizations.HasStatusAsync(authorization, Statuses.Valid, context.RequestAborted)
                || !await authorizations.HasTypeAsync(authorization, AuthorizationTypes.Permanent, context.RequestAborted))
            {
                continue;
            }

            string? applicationId = await authorizations.GetApplicationIdAsync(authorization, context.RequestAborted);
            object? application = applicationId is null
                ? null
                : await applications.FindByIdAsync(applicationId, context.RequestAborted);
            ImmutableArray<string> scopes = await authorizations.GetScopesAsync(authorization, context.RequestAborted);
            allowed.Add(new AllowedApplicationView(
                (await authorizations.GetIdAsync(authorization, context.RequestAborted))!,
                application is null
                    ? null
                    : await applications.GetDisplayNameAsync(application, context.RequestAborted),
                scopes,
                await authorizations.GetCreationDateAsync(authorization, context.RequestAborted)));
        }

        return Results.Ok(allowed.OrderByDescending(entry => entry.CreatedAt).ToList());
    }

    /// <summary>Withdraws an allowance and revokes the tokens issued under it.</summary>
    private static async Task<IResult> RevokeAsync(
        HttpContext context,
        IOpenIddictAuthorizationManager authorizations,
        IOpenIddictTokenManager tokens,
        string authorizationId)
    {
        if (await OAuthAccount.CurrentAsync(context) is not { } account)
        {
            return OAuthAccount.SignInRequired();
        }

        if (!OAuthAccount.IsSameOrigin(context))
        {
            return OAuthAccount.ForeignOrigin();
        }

        // Only the account's own authorizations are searched, so another account's identifier is simply absent.
        await foreach (object authorization in authorizations.FindBySubjectAsync(account.Id.ToString(),
                           context.RequestAborted))
        {
            string? id = await authorizations.GetIdAsync(authorization, context.RequestAborted);
            if (id is null || !string.Equals(id, authorizationId, StringComparison.Ordinal))
            {
                continue;
            }

            await authorizations.TryRevokeAsync(authorization, context.RequestAborted);
            await tokens.RevokeByAuthorizationIdAsync(id, context.RequestAborted);
            return Results.NoContent();
        }

        return Results.NotFound();
    }
}