< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Security.JwtApplicationTokenService
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Security/JwtApplicationTokenService.cs
Line coverage
100%
Covered lines: 51
Uncovered lines: 0
Coverable lines: 51
Total lines: 105
Line coverage: 100%
Branch coverage
94%
Covered branches: 17
Total branches: 18
Branch coverage: 94.4%
Method coverage
100%
Covered methods: 5
Fully covered methods: 5
Total methods: 5
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%
Claim(...)75%44100%

File(s)

/home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Security/JwtApplicationTokenService.cs

#LineLine coverage
 1using ArturRios.Heimdall.Command.Services;
 2using ArturRios.Heimdall.Shared.Security;
 3using ArturRios.Jwt;
 4using Microsoft.IdentityModel.JsonWebTokens;
 5using Microsoft.IdentityModel.Tokens;
 6
 7namespace ArturRios.Heimdall.WebApi.Security;
 8
 9/// <summary>
 10///     Issues and validates application tokens (UC-56, UC-57, FR-AP-13): ES256, signed with the same
 11///     key as person tokens, under the application audience.
 12/// </summary>
 13/// <remarks>
 14///     <para>
 15///         <b>Signing</b> goes through <see cref="JwtHandler" /> with the person configuration, so the
 16///         current key signs and a rotation (<c>HEIMDALL_AUTH_SIGNING_KEY_PREVIOUS</c>) covers
 17///         application tokens exactly as it covers person tokens. Only the audience, the lifetime and
 18///         the claims differ. No <c>roleId</c> is written: an application holds no Heimdall role.
 19///     </para>
 20///     <para>
 21///         <b>Validating</b> cannot go through <see cref="JwtHandler.IsTokenValidAsync(string, IEnumerable{JwtKey})" />
 22///         which checks only the signature and lifetime. Here the issuer, the audience and the kind
 23///         are the point (TH-35), so the token is validated with every parameter set and no clock
 24///         skew, under the configured EC keys and ES256 alone. The standard issuer check runs only
 25///         when one is configured: <c>HEIMDALL_AUTH_TOKEN_ISSUER</c> defaults to empty, and an empty
 26///         expected issuer would refuse every token. <see cref="TokenOrigin" /> then holds the token to
 27///         exactly what Heimdall issues — the configured issuer, or none when none is configured, and
 28///         the application audience as its only audience — so a token signed under another issuer is
 29///         refused either way (TB-5). An HS256 token is refused, whatever it is signed with: no HMAC
 30///         key is held, and HS256 is not a valid algorithm (Threat Model TH-43).
 31///     </para>
 32/// </remarks>
 14733public class JwtApplicationTokenService(
 14734    JwtConfiguration configuration,
 14735    JwtHandler jwtHandler,
 14736    ApplicationTokenOptions options) : IApplicationTokenIssuer, IApplicationTokenValidator
 37{
 238    private static readonly JsonWebTokenHandler Reader = new();
 39
 40    // The configured ES256 keys, the current and any previous one, as IdentityModel holds them.
 35141    private readonly IReadOnlyList<SecurityKey> _keys = [.. configuration.Keys.Select(key => key.ToSecurityKey())];
 42
 43    public Task<AuthToken> IssueAsync(ApplicationTokenSubject subject)
 44    {
 1545        var claims = new Dictionary<string, string>
 1546        {
 1547            [ApplicationTokenClaims.TokenKind] = ApplicationTokenClaims.ApplicationKind,
 1548            [ApplicationTokenClaims.Id] = subject.ApplicationId.ToString(),
 1549            [ApplicationTokenClaims.ScopeId] = subject.ScopeId.ToString(),
 1550            [ApplicationTokenClaims.CredentialId] = subject.CredentialId.ToString(),
 1551            [ApplicationTokenClaims.Name] = subject.Name
 1552        };
 53
 1554        var tokenConfiguration = configuration with
 1555        {
 1556            Audience = options.Audience,
 1557            ExpirationInSeconds = options.Lifetime.TotalSeconds,
 1558            Claims = claims
 1559        };
 60
 1561        return Task.FromResult(new AuthToken(
 1562            jwtHandler.CreateToken(tokenConfiguration),
 1563            DateTime.UtcNow.Add(options.Lifetime)));
 64    }
 65
 66    public async Task<ApplicationTokenPrincipal?> ValidateAsync(string? token)
 67    {
 4268        if (string.IsNullOrWhiteSpace(token))
 69        {
 370            return null;
 71        }
 72
 3973        var result = await Reader.ValidateTokenAsync(token, new TokenValidationParameters
 3974        {
 3975            ValidIssuer = configuration.Issuer,
 3976            ValidateIssuer = !string.IsNullOrWhiteSpace(configuration.Issuer),
 3977            ValidAudience = options.Audience,
 3978            ValidateAudience = true,
 3979            IssuerSigningKeys = _keys,
 3980            ValidateIssuerSigningKey = true,
 3981            // Defence in depth, kept on purpose: the keys are EC keys alone, so an HS256, HS384 or
 3982            // HS512 token cannot verify against any of them, and this pin would refuse one even if an
 3983            // HMAC key were ever held.
 3984            ValidAlgorithms = [SecurityAlgorithms.EcdsaSha256],
 3985            ValidateLifetime = true,
 3986            RequireExpirationTime = true,
 3987            ClockSkew = TimeSpan.Zero
 3988        });
 89
 3990        if (!result.IsValid ||
 3991            !TokenOrigin.IsIssuedFor(token, configuration.Issuer, options.Audience) ||
 3992            Claim(result, ApplicationTokenClaims.TokenKind) != ApplicationTokenClaims.ApplicationKind ||
 3993            !Guid.TryParse(Claim(result, ApplicationTokenClaims.Id), out var applicationId) ||
 3994            !Guid.TryParse(Claim(result, ApplicationTokenClaims.ScopeId), out var scopeId) ||
 3995            !Guid.TryParse(Claim(result, ApplicationTokenClaims.CredentialId), out var credentialId))
 96        {
 2697            return null;
 98        }
 99
 13100        return new ApplicationTokenPrincipal(applicationId, scopeId, credentialId, result.SecurityToken.ValidTo);
 42101    }
 102
 103    private static string? Claim(TokenValidationResult result, string name) =>
 57104        result.Claims.TryGetValue(name, out var value) ? value?.ToString() : null;
 105}