< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Security.JwtApplicationTokenService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/JwtApplicationTokenService.cs
Line coverage
100%
Covered lines: 51
Uncovered lines: 0
Coverable lines: 51
Total lines: 108
Line coverage: 100%
Branch coverage
90%
Covered branches: 18
Total branches: 20
Branch coverage: 90%
Method coverage
100%
Covered methods: 6
Fully covered methods: 6
Total methods: 6
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
.cctor()100%11100%
IssueAsync(...)100%11100%
ValidateAsync()100%1414100%
SigningKeys()50%22100%
Claim(...)75%44100%

File(s)

/home/runner/work/heimdall-api/heimdall-api/src/Presentation/ArturRios.Heimdall.WebApi/Security/JwtApplicationTokenService.cs

#LineLine coverage
 1using System.Text;
 2using ArturRios.Heimdall.Command.Services;
 3using ArturRios.Heimdall.Shared.Security;
 4using ArturRios.Jwt;
 5using Microsoft.IdentityModel.JsonWebTokens;
 6using Microsoft.IdentityModel.Tokens;
 7
 8namespace ArturRios.Heimdall.WebApi.Security;
 9
 10/// <summary>
 11///     Issues and validates application tokens (UC-56, UC-57, FR-AP-13): HMAC-SHA256, signed with
 12///     the same key pair as person tokens, under the application audience.
 13/// </summary>
 14/// <remarks>
 15///     <para>
 16///         <b>Signing</b> goes through <see cref="JwtHandler" /> with the person configuration, so the
 17///         current key signs and a rotation (<c>HEIMDALL_AUTH_TOKEN_SECRET_PREVIOUS</c>) covers
 18///         application tokens exactly as it covers person tokens. Only the audience, the lifetime and
 19///         the claims differ. No <c>roleId</c> is written: an application holds no Heimdall role.
 20///     </para>
 21///     <para>
 22///         <b>Validating</b> cannot go through <see cref="JwtHandler.IsTokenValidAsync(string, IEnumerable{JwtKey})" />
 23///         which checks only the signature and lifetime. Here the issuer, the audience and the kind
 24///         are the point (TH-35), so the token is validated with every parameter set and no clock
 25///         skew. Keys are read as ASCII, as <c>ArturRios.Jwt</c> writes them. The standard issuer
 26///         check runs only when one is configured: <c>HEIMDALL_AUTH_TOKEN_ISSUER</c> defaults to
 27///         empty, and an empty expected issuer would refuse every token. <see cref="TokenOrigin" />
 28///         then holds the token to exactly what Heimdall issues — the configured issuer, or none when
 29///         none is configured, and the application audience as its only audience — so a token signed
 30///         with the shared key under another issuer is refused either way (TB-5).
 31///     </para>
 32/// </remarks>
 5233public class JwtApplicationTokenService(
 5234    JwtConfiguration configuration,
 5235    JwtHandler jwtHandler,
 5236    ApplicationTokenOptions options) : IApplicationTokenIssuer, IApplicationTokenValidator
 37{
 238    private static readonly JsonWebTokenHandler Reader = new();
 39
 40    public Task<AuthToken> IssueAsync(ApplicationTokenSubject subject)
 41    {
 1442        var claims = new Dictionary<string, string>
 1443        {
 1444            [ApplicationTokenClaims.TokenKind] = ApplicationTokenClaims.ApplicationKind,
 1445            [ApplicationTokenClaims.Id] = subject.ApplicationId.ToString(),
 1446            [ApplicationTokenClaims.ScopeId] = subject.ScopeId.ToString(),
 1447            [ApplicationTokenClaims.CredentialId] = subject.CredentialId.ToString(),
 1448            [ApplicationTokenClaims.Name] = subject.Name
 1449        };
 50
 1451        var tokenConfiguration = configuration with
 1452        {
 1453            Audience = options.Audience,
 1454            ExpirationInSeconds = options.Lifetime.TotalSeconds,
 1455            Claims = claims
 1456        };
 57
 1458        return Task.FromResult(new AuthToken(
 1459            jwtHandler.CreateToken(tokenConfiguration),
 1460            DateTime.UtcNow.Add(options.Lifetime)));
 61    }
 62
 63    public async Task<ApplicationTokenPrincipal?> ValidateAsync(string? token)
 64    {
 2965        if (string.IsNullOrWhiteSpace(token))
 66        {
 367            return null;
 68        }
 69
 2670        var result = await Reader.ValidateTokenAsync(token, new TokenValidationParameters
 2671        {
 2672            ValidIssuer = configuration.Issuer,
 2673            ValidateIssuer = !string.IsNullOrWhiteSpace(configuration.Issuer),
 2674            ValidAudience = options.Audience,
 2675            ValidateAudience = true,
 2676            IssuerSigningKeys = SigningKeys(),
 2677            ValidateIssuerSigningKey = true,
 2678            ValidAlgorithms = [SecurityAlgorithms.HmacSha256],
 2679            ValidateLifetime = true,
 2680            RequireExpirationTime = true,
 2681            ClockSkew = TimeSpan.Zero
 2682        });
 83
 2684        if (!result.IsValid ||
 2685            !TokenOrigin.IsIssuedFor(token, configuration.Issuer, options.Audience) ||
 2686            Claim(result, ApplicationTokenClaims.TokenKind) != ApplicationTokenClaims.ApplicationKind ||
 2687            !Guid.TryParse(Claim(result, ApplicationTokenClaims.Id), out var applicationId) ||
 2688            !Guid.TryParse(Claim(result, ApplicationTokenClaims.ScopeId), out var scopeId) ||
 2689            !Guid.TryParse(Claim(result, ApplicationTokenClaims.CredentialId), out var credentialId))
 90        {
 1591            return null;
 92        }
 93
 1194        return new ApplicationTokenPrincipal(applicationId, scopeId, credentialId, result.SecurityToken.ValidTo);
 2995    }
 96
 97    private IEnumerable<SecurityKey> SigningKeys()
 98    {
 2699        var secrets = configuration.Keys.Count > 0
 109100            ? configuration.Keys.Select(key => key.Secret)
 26101            : [configuration.Secret];
 102
 135103        return secrets.Select(secret => new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret)));
 104    }
 105
 106    private static string? Claim(TokenValidationResult result, string name) =>
 49107        result.Claims.TryGetValue(name, out var value) ? value?.ToString() : null;
 108}