< Summary

Information
Class: Api.Auth.Handlers.AuthHandler.Token
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Handlers/AuthHandler.cs
Line coverage
100%
Covered lines: 1
Uncovered lines: 0
Coverable lines: 1
Total lines: 183
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Handlers/AuthHandler.cs

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <alper@alper-celik.dev>
 2//
 3// SPDX-License-Identifier: AGPL-3.0-or-later
 4
 5using System.Buffers.Text;
 6using System.Security.Claims;
 7using System.Text.Encodings.Web;
 8using Api.Auth.Models;
 9using Api.Auth.Utils;
 10using Api.Database;
 11using Api.Database.Utils;
 12using Geralt;
 13using Microsoft.AspNetCore.Authentication;
 14using Microsoft.EntityFrameworkCore;
 15using Microsoft.Extensions.Options;
 16
 17namespace Api.Auth.Handlers;
 18
 19class AuthHandler(
 20    IOptionsMonitor<AuthenticationSchemeOptions> options,
 21    ILoggerFactory logger,
 22    UrlEncoder encoder,
 23    PGContext db
 24) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
 25{
 26    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 27    {
 28        string?[] tokens =
 29        [
 30            Context.Request.Headers.Authorization.FirstOrDefault(),
 31            Context.Request.Cookies[AuthUtils.TokenCookieName],
 32        ];
 33        var tokenHash = SelectToken(tokens);
 34
 35        if (tokenHash is null)
 36        {
 37            return AuthenticateResult.NoResult();
 38        }
 39
 40        var identity = await (
 41            tokenHash.Prefix switch
 42            {
 43                AuthUtils.UserTokenPrefixName => GetUserIdentityAsync(
 44                    tokenHash.TokenHash
 45                ),
 46
 47                AuthUtils.RemoteServiceTokenPrefixName => GetServiceIdentity(
 48                    tokenHash.TokenHash,
 49                    Context
 50                        .Request.Headers[RemoteServiceToken.ServiceImpersonatedUserHeader]
 51                        .FirstOrDefault()
 52                        ?.TryParseGuid()
 53                ),
 54                _ => Task.FromResult<ClaimsIdentity?>(null),
 55            }
 56        );
 57
 58        if (identity is not null)
 59        {
 60            return AuthenticateResult.Success(
 61                new AuthenticationTicket(
 62                    new ClaimsPrincipal(new ClaimsPrincipal(identity)),
 63                    Scheme.Name
 64                )
 65            );
 66        }
 67
 68        return AuthenticateResult.NoResult();
 69    }
 70
 171    private record Token(byte[] TokenHash, string Prefix);
 72
 73    private static Token? SelectToken(string?[] tokens)
 74    {
 75        var tokenHashes = tokens
 76            .Where(s =>
 77                s != null && AuthUtils.TokenPrefixesNames.Any(p => s.StartsWith(p))
 78            )
 79            .Select(s =>
 80            {
 81                if (s is null)
 82                {
 83                    return null;
 84                }
 85                var prefix = s.Split(AuthUtils.PrefixSeparator).First();
 86                var apiToken = Base64Url.DecodeFromChars(
 87                    s.AsSpan()[(prefix.Length + 1)..]
 88                );
 89
 90                var tokenHash = new byte[32];
 91                BLAKE2b.ComputeHash(tokenHash, apiToken);
 92                return new Token(tokenHash, prefix);
 93            })
 94            .Where(t => t is not null)
 95            .ToArray();
 96
 97        return tokenHashes.Length == 0 ? null : tokenHashes.First();
 98    }
 99
 100    private async Task<ClaimsIdentity?> GetUserIdentityAsync(byte[] tokenHash)
 101    {
 102        var userToken = await db
 103            .UserTokens.Where(ut => tokenHash.SequenceEqual(ut.TokenHash))
 104            .FirstOrDefaultAsync();
 105
 106        if (userToken is null)
 107        {
 108            return null;
 109        }
 110        userToken = AuthUtils.UpdateLastUsedTokens(userToken);
 111
 112        await db.SaveChangesAsync();
 113
 114        Claim[] claims =
 115        [
 116            new Claim(ClaimTypes.NameIdentifier, userToken.UserId.ToString()),
 117            new Claim(
 118                UserTokenEF.PermissionBitsType,
 119                ((long)userToken.Permissions).ToString()
 120            ),
 121        ];
 122        return new ClaimsIdentity(claims, "user_token");
 123    }
 124
 125    private async Task<ClaimsIdentity?> GetServiceIdentity(
 126        byte[] tokenHash,
 127        Guid? targetUser
 128    )
 129    {
 130        var serviceToken = await db
 131            .RemoteServiceTokens.Where(t => t.TokenHash.SequenceEqual(tokenHash))
 132            .Include(t => t.RemoteService)
 133                .ThenInclude(t => t.UserPermissions.Where(u => u.UserId == targetUser))
 134            .FirstOrDefaultAsync();
 135
 136        var userPermission = serviceToken?.RemoteService.UserPermissions.FirstOrDefault();
 137
 138        if (serviceToken is null)
 139        {
 140            return null;
 141        }
 142
 143        AuthUtils.UpdateLastUsedTokens(serviceToken);
 144
 145        if (userPermission is not null)
 146        {
 147            AuthUtils.UpdateLastUsedTokens(userPermission);
 148        }
 149
 150        await db.SaveChangesAsync();
 151
 152        List<Claim> claims =
 153        [
 154            new Claim(
 155                RemoteServiceToken.ServiceIdentifierType,
 156                serviceToken.RemoteServiceId.ToString()
 157            ),
 158        ];
 159        if (serviceToken.RemoteService.AllowedJobs.Count != 0)
 160        {
 161            claims.Add(
 162                new Claim(
 163                    RemoteServiceToken.ServiceAllowedJobsType,
 164                    serviceToken.RemoteService.AllowedJobs.Aggregate(
 165                        (s1, s2) => $"{s1},{s2}"
 166                    )
 167                )
 168            );
 169        }
 170        if (userPermission is not null)
 171        {
 172            claims.AddRange([
 173                new Claim(ClaimTypes.NameIdentifier, userPermission.UserId.ToString()),
 174                new Claim(
 175                    UserTokenEF.PermissionBitsType,
 176                    ((long)userPermission.Permissions).ToString()
 177                ),
 178            ]);
 179        }
 180
 181        return new ClaimsIdentity(claims, "service_token");
 182    }
 183}

Methods/Properties

.ctor(byte[], string)