< Summary - Heimdall API

Information
Class: ArturRios.Heimdall.WebApi.Security.JwtAuthTokenIssuer
Assembly: ArturRios.Heimdall.WebApi
File(s): /home/ci/agent/workspace/heimdall-api-docs_main/app/src/Presentation/ArturRios.Heimdall.WebApi/Security/JwtAuthTokenIssuer.cs
Line coverage
100%
Covered lines: 42
Uncovered lines: 0
Coverable lines: 42
Total lines: 106
Line coverage: 100%
Branch coverage
100%
Covered branches: 4
Total branches: 4
Branch coverage: 100%
Method coverage
100%
Covered methods: 4
Fully covered methods: 4
Total methods: 4
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
IssueAsync()100%11100%
IssueSubjectRightsOnlyAsync(...)100%11100%
LoadScopePermissionClaimsAsync()100%44100%

File(s)

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

#LineLine coverage
 1using ArturRios.Data.Relational.Core.Interfaces;
 2using ArturRios.Heimdall.Command.Services;
 3using ArturRios.Heimdall.Domain.Entities;
 4using ArturRios.Jwt;
 5using ArturRios.Util.WebApi.Security.Interfaces;
 6using Microsoft.EntityFrameworkCore;
 7
 8namespace ArturRios.Heimdall.WebApi.Security;
 9
 10/// <summary>
 11///     Issues UC-11's authentication token as the application's own ES256-signed JWT (FR-AU-03),
 12///     using the same <see cref="IAuthenticatedUserMapper" /> the middleware validates incoming
 13///     tokens with — so a token this class issues is one the API can always read back.
 14/// </summary>
 15/// <remarks>
 16///     The token is signed with the current key, which its <c>kid</c> names by thumbprint
 17///     (<see cref="TokenSigningKeys" />), as is every other kind Heimdall issues. Expiry comes from
 18///     the registered <see cref="JwtConfiguration" />, which reads it from the
 19///     environment (NFR-03), and is reported to the caller so a client need not decode the token to
 20///     know when to re-authenticate. At issuance the acting scope's permissions whose
 21///     <see cref="ScopePermission.IncludeAsJwtClaim" /> flag is set are read from the database
 22///     (UC-31…UC-35, FR-SP) and folded into the token, so a downstream caller can authorize on them.
 23///     A User claims the permissions of the scope they belong to; a Scope Admin the union over the
 24///     scopes they own; a System Admin none, since they belong to no scope. Logically deleted scopes
 25///     and permissions are excluded, as a deleted resource is never claimed. A restricted identity
 26///     is issued a subject-rights-only token instead (NFR-24), which claims none of this, under
 27///     <see cref="PersonTokenAudiences.SubjectRights" /> rather than the full token's audience, so a
 28///     consumer pinning the person audience refuses it (Threat Model TH-35).
 29/// </remarks>
 31030public class JwtAuthTokenIssuer(
 31031    JwtConfiguration configuration,
 31032    PersonTokenAudiences audiences,
 31033    JwtHandler jwtHandler,
 31034    IAuthenticatedUserMapper mapper,
 31035    IAsyncReadOnlyRepository<ScopePermission, long> permissionReader) : IAuthTokenIssuer
 36{
 37    public async Task<AuthToken> IssueAsync(AuthTokenSubject subject)
 38    {
 8639        var permissionClaims = await LoadScopePermissionClaimsAsync(subject);
 40
 41        // The issued token carries this login's claims; everything else — key, issuer, audience,
 42        // lifetime — comes from the configuration the validator also uses.
 8643        var user = new IdentityUser(
 8644            subject.PersonId, subject.RoleId, subject.ScopeId, subject.OwnedScopeIds)
 8645        {
 8646            DisplayName = subject.DisplayName,
 8647            ScopePermissionClaims = permissionClaims,
 8648            SecurityStamp = subject.SecurityStamp
 8649        };
 50
 8651        var tokenConfiguration = configuration with { Claims = mapper.ToClaims(user) };
 52
 8653        return new AuthToken(
 8654            jwtHandler.CreateToken(tokenConfiguration),
 8655            DateTime.UtcNow.AddSeconds(configuration.ExpirationInSeconds));
 8656    }
 57
 58    public Task<AuthToken> IssueSubjectRightsOnlyAsync(Guid subjectId, int roleId, Guid? securityStamp)
 59    {
 60        // Built from the subject and role alone: the token admits nothing a scope, an ownership or
 61        // a permission would matter to, so it claims none — and no display name, since it is not
 62        // a token any downstream service should provision a profile from. The stamp it does carry,
 63        // so a credential change revokes it like any other person token (FR-AU-11).
 1364        var user = new IdentityUser(subjectId, roleId) { SubjectRightsOnly = true, SecurityStamp = securityStamp };
 65
 1366        var tokenConfiguration = configuration with
 1367        {
 1368            Audience = audiences.SubjectRights,
 1369            Claims = mapper.ToClaims(user)
 1370        };
 71
 1372        return Task.FromResult(new AuthToken(
 1373            jwtHandler.CreateToken(tokenConfiguration),
 1374            DateTime.UtcNow.AddSeconds(configuration.ExpirationInSeconds)));
 75    }
 76
 77    /// <summary>
 78    ///     The distinct names of the non-deleted permissions flagged <see cref="ScopePermission.IncludeAsJwtClaim" />
 79    ///     across the scopes this subject acts within — <see cref="AuthTokenSubject.ScopeId" /> for a
 80    ///     User and <see cref="AuthTokenSubject.OwnedScopeIds" /> for a Scope Admin. Empty for a
 81    ///     System Admin, who carries no scope.
 82    /// </summary>
 83    private async Task<IReadOnlyCollection<string>> LoadScopePermissionClaimsAsync(AuthTokenSubject subject)
 84    {
 8685        var scopePublicIds = subject.OwnedScopeIds.ToList();
 86
 8687        if (subject.ScopeId is not null)
 88        {
 2889            scopePublicIds.Add(subject.ScopeId.Value);
 90        }
 91
 8692        if (scopePublicIds.Count == 0)
 93        {
 5594            return [];
 95        }
 96
 3197        return await permissionReader.Query()
 3198            .Where(permission => scopePublicIds.Contains(permission.Scope.PublicId)
 3199                                 && !permission.Scope.IsDeleted
 31100                                 && !permission.IsDeleted
 31101                                 && permission.IncludeAsJwtClaim)
 31102            .Select(permission => permission.Name)
 31103            .Distinct()
 31104            .ToListAsync();
 86105    }
 106}