< Summary

Information
Class: Anichron.API.Services.TokenService
Assembly: Anichron.API
File(s): /home/runner/work/anichron/anichron/src/Anichron.API/Services/TokenService.cs
Tag: 228_36821334185
Line coverage
100%
Covered lines: 56
Uncovered lines: 0
Coverable lines: 56
Total lines: 104
Line coverage: 100%
Branch coverage
100%
Covered branches: 16
Total branches: 16
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
IssueAsync()100%11100%
RefreshAsync()100%1212100%
RevokeAsync()100%44100%
MarkAllSessionsRevokedAsync(...)100%11100%
GenerateRefreshToken()100%11100%
HashToken(...)100%11100%

File(s)

/home/runner/work/anichron/anichron/src/Anichron.API/Services/TokenService.cs

#LineLine coverage
 1using Anichron.API.Security;
 2using Anichron.API.Settings;
 3using Anichron.Core.Data;
 4using Anichron.Core.Data.Repository;
 5using Anichron.Core.Domain;
 6using Microsoft.Extensions.Options;
 7using System.Security.Cryptography;
 8
 9namespace Anichron.API.Services;
 10
 11public interface ITokenService
 12{
 13    Task<AuthTokens> IssueAsync(User user, CancellationToken ct);
 14    Task<AuthResult<AuthTokens>> RefreshAsync(string rawToken, CancellationToken ct);
 15    Task RevokeAsync(string rawToken, CancellationToken ct);
 16    Task MarkAllSessionsRevokedAsync(Guid userId, Instant revokedAt, CancellationToken ct);
 17}
 18
 1519public sealed class TokenService(
 1520    IRefreshTokenRepository tokens,
 1521    IUnitOfWork unitOfWork,
 1522    IClock clock,
 1523    IGuidFactory guidFactory,
 1524    IOptions<JwtSettings> options,
 1525    IJwtFactory jwtFactory) : ITokenService
 26{
 1527    private readonly JwtSettings settings = options.Value;
 28
 29    public async Task<AuthTokens> IssueAsync(User user, CancellationToken ct)
 630    {
 631        var rawToken = GenerateRefreshToken();
 632        var now = clock.GetCurrentInstant();
 33
 634        tokens.Add(new RefreshToken
 635        {
 636            Id = guidFactory.NewGuid(),
 637            UserId = user.Id,
 638            TokenHash = HashToken(rawToken),
 639            CreatedAt = now,
 640            ExpiresAt = now.Plus(Duration.FromDays(settings.RefreshTokenDays)),
 641        });
 42
 643        await unitOfWork.SaveChangesAsync(ct);
 44
 545        return new AuthTokens(jwtFactory.Create(user), rawToken);
 546    }
 47
 48    public async Task<AuthResult<AuthTokens>> RefreshAsync(string rawToken, CancellationToken ct)
 749    {
 750        var tokenHash = HashToken(rawToken);
 751        var now = clock.GetCurrentInstant();
 52
 753        var stored = await tokens.FindByHashWithUserAsync(tokenHash, ct);
 54
 755        if (stored is null)
 156            return AuthResult.Fail<AuthTokens>(AuthError.TokenInvalid);
 57
 658        if (stored.RevokedAt.HasValue)
 159        {
 60            // Revoked token replayed — possible theft, wipe all sessions
 161            await tokens.RevokeAllActiveByUserIdAsync(stored.UserId, now, ct);
 162            return AuthResult.Fail<AuthTokens>(AuthError.TokenInvalid);
 63        }
 64
 565        if (stored.ExpiresAt <= now)
 166            return AuthResult.Fail<AuthTokens>(AuthError.TokenInvalid);
 67
 468        if (stored.User.IsDisabled)
 169            return AuthResult.Fail<AuthTokens>(AuthError.AccountDisabled);
 70
 371        if (stored.User.LockedUntil is { } lockedUntil && lockedUntil > now)
 172        {
 173            return AuthResult.Locked<AuthTokens>(
 174                Math.Max(1, (int)Math.Ceiling((lockedUntil - now).TotalSeconds)));
 75        }
 76
 77        // Mark old token revoked; IssueAsync's SaveChangesAsync persists both
 278        stored.RevokedAt = now;
 79
 280        return AuthResult.Ok(await IssueAsync(stored.User, ct));
 681    }
 82
 83    public async Task RevokeAsync(string rawToken, CancellationToken ct)
 384    {
 385        var tokenHash = HashToken(rawToken);
 386        var stored = await tokens.FindByHashAsync(tokenHash, ct);
 387        if (stored is null)
 188            return;
 289        if (stored.RevokedAt.HasValue)
 190            return;
 91
 192        stored.RevokedAt = clock.GetCurrentInstant();
 193        await unitOfWork.SaveChangesAsync(ct);
 394    }
 95
 96    public Task MarkAllSessionsRevokedAsync(Guid userId, Instant revokedAt, CancellationToken ct)
 197        => tokens.RevokeAllActiveByUserIdAsync(userId, revokedAt, ct);
 98
 99    private static string GenerateRefreshToken()
 6100        => Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
 101
 102    private static string HashToken(string rawToken)
 16103        => Convert.ToBase64String(SHA256.HashData(Convert.FromBase64String(rawToken)));
 104}