< Summary

Information
Class: Api.Auth.Handlers.AuthHandler
Assembly: Api
File(s): /home/runner/work/ProjectRead.ing/ProjectRead.ing/Api/Auth/Handlers/AuthHandler.cs
Line coverage
96%
Covered lines: 123
Uncovered lines: 5
Coverable lines: 128
Total lines: 183
Line coverage: 96%
Branch coverage
91%
Covered branches: 33
Total branches: 36
Branch coverage: 91.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
HandleAuthenticateAsync()90%101097.3%
.ctor(...)100%11100%
SelectToken(...)100%88100%
GetUserIdentityAsync()50%2289.47%
GetServiceIdentity()100%1212100%

File(s)

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

#LineLine coverage
 1// SPDX-FileCopyrightText: 2026 Alper Çelik <[email protected]>
 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
 124) : AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
 25{
 26    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
 127    {
 128        string?[] tokens =
 129        [
 130            Context.Request.Headers.Authorization.FirstOrDefault(),
 131            Context.Request.Cookies[AuthUtils.TokenCookieName],
 132        ];
 133        var tokenHash = SelectToken(tokens);
 34
 135        if (tokenHash is null)
 136        {
 137            return AuthenticateResult.NoResult();
 38        }
 39
 140        var identity = await (
 141            tokenHash.Prefix switch
 142            {
 143                AuthUtils.UserTokenPrefixName => GetUserIdentityAsync(
 144                    tokenHash.TokenHash
 145                ),
 146
 147                AuthUtils.RemoteServiceTokenPrefixName => GetServiceIdentity(
 148                    tokenHash.TokenHash,
 149                    Context
 150                        .Request.Headers[RemoteServiceToken.ServiceImpersonatedUserHeader]
 151                        .FirstOrDefault()
 152                        ?.TryParseGuid()
 153                ),
 054                _ => Task.FromResult<ClaimsIdentity?>(null),
 155            }
 156        );
 57
 158        if (identity is not null)
 159        {
 160            return AuthenticateResult.Success(
 161                new AuthenticationTicket(
 162                    new ClaimsPrincipal(new ClaimsPrincipal(identity)),
 163                    Scheme.Name
 164                )
 165            );
 66        }
 67
 168        return AuthenticateResult.NoResult();
 169    }
 70
 171    private record Token(byte[] TokenHash, string Prefix);
 72
 73    private static Token? SelectToken(string?[] tokens)
 174    {
 175        var tokenHashes = tokens
 176            .Where(s =>
 177                s != null && AuthUtils.TokenPrefixesNames.Any(p => s.StartsWith(p))
 178            )
 179            .Select(s =>
 180            {
 181                if (s is null)
 082                {
 083                    return null;
 184                }
 185                var prefix = s.Split(AuthUtils.PrefixSeparator).First();
 186                var apiToken = Base64Url.DecodeFromChars(
 187                    s.AsSpan()[(prefix.Length + 1)..]
 188                );
 189
 190                var tokenHash = new byte[32];
 191                BLAKE2b.ComputeHash(tokenHash, apiToken);
 192                return new Token(tokenHash, prefix);
 193            })
 194            .Where(t => t is not null)
 195            .ToArray();
 96
 197        return tokenHashes.Length == 0 ? null : tokenHashes.First();
 198    }
 99
 100    private async Task<ClaimsIdentity?> GetUserIdentityAsync(byte[] tokenHash)
 1101    {
 1102        var userToken = await db
 1103            .UserTokens.Where(ut => tokenHash.SequenceEqual(ut.TokenHash))
 1104            .FirstOrDefaultAsync();
 105
 1106        if (userToken is null)
 0107        {
 0108            return null;
 109        }
 1110        userToken = AuthUtils.UpdateLastUsedTokens(userToken);
 111
 1112        await db.SaveChangesAsync();
 113
 1114        Claim[] claims =
 1115        [
 1116            new Claim(ClaimTypes.NameIdentifier, userToken.UserId.ToString()),
 1117            new Claim(
 1118                UserTokenEF.PermissionBitsType,
 1119                ((long)userToken.Permissions).ToString()
 1120            ),
 1121        ];
 1122        return new ClaimsIdentity(claims, "user_token");
 1123    }
 124
 125    private async Task<ClaimsIdentity?> GetServiceIdentity(
 126        byte[] tokenHash,
 127        Guid? targetUser
 128    )
 1129    {
 1130        var serviceToken = await db
 1131            .RemoteServiceTokens.Where(t => t.TokenHash.SequenceEqual(tokenHash))
 1132            .Include(t => t.RemoteService)
 1133                .ThenInclude(t => t.UserPermissions.Where(u => u.UserId == targetUser))
 1134            .FirstOrDefaultAsync();
 135
 1136        var userPermission = serviceToken?.RemoteService.UserPermissions.FirstOrDefault();
 137
 1138        if (serviceToken is null)
 1139        {
 1140            return null;
 141        }
 142
 1143        AuthUtils.UpdateLastUsedTokens(serviceToken);
 144
 1145        if (userPermission is not null)
 1146        {
 1147            AuthUtils.UpdateLastUsedTokens(userPermission);
 1148        }
 149
 1150        await db.SaveChangesAsync();
 151
 1152        List<Claim> claims =
 1153        [
 1154            new Claim(
 1155                RemoteServiceToken.ServiceIdentifierType,
 1156                serviceToken.RemoteServiceId.ToString()
 1157            ),
 1158        ];
 1159        if (serviceToken.RemoteService.AllowedJobs.Count != 0)
 1160        {
 1161            claims.Add(
 1162                new Claim(
 1163                    RemoteServiceToken.ServiceAllowedJobsType,
 1164                    serviceToken.RemoteService.AllowedJobs.Aggregate(
 1165                        (s1, s2) => $"{s1},{s2}"
 1166                    )
 1167                )
 1168            );
 1169        }
 1170        if (userPermission is not null)
 1171        {
 1172            claims.AddRange([
 1173                new Claim(ClaimTypes.NameIdentifier, userPermission.UserId.ToString()),
 1174                new Claim(
 1175                    UserTokenEF.PermissionBitsType,
 1176                    ((long)userPermission.Permissions).ToString()
 1177                ),
 1178            ]);
 1179        }
 180
 1181        return new ClaimsIdentity(claims, "service_token");
 1182    }
 183}