< Summary

Information
Class: ArturRios.Util.WebApi.Security.Authentication.JwtTokenValidator
Assembly: ArturRios.Util.WebApi
File(s): /home/ci/agent/workspace/dotnet-webapi-util-docs_main/app/src/Security/Authentication/JwtTokenValidator.cs
Line coverage
93%
Covered lines: 58
Uncovered lines: 4
Coverable lines: 62
Total lines: 174
Line coverage: 93.5%
Branch coverage
97%
Covered branches: 35
Total branches: 36
Branch coverage: 97.2%
Method coverage
100%
Covered methods: 7
Fully covered methods: 5
Total methods: 7
Method coverage: 100%
Full method coverage: 71.4%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
ValidateAsync()93.75%161695.23%
CheckIssuerAndAudience(...)100%141275%
EnsureExpectedValuesConfigured(...)100%88100%
MapUser(...)100%11100%
MapId(...)100%11100%

File(s)

/home/ci/agent/workspace/dotnet-webapi-util-docs_main/app/src/Security/Authentication/JwtTokenValidator.cs

#LineLine coverage
 1using System.IdentityModel.Tokens.Jwt;
 2using ArturRios.Jwt;
 3using ArturRios.Util.WebApi.Security.Configuration;
 4using ArturRios.Util.WebApi.Security.Enums;
 5using ArturRios.Util.WebApi.Security.Interfaces;
 6using ArturRios.Util.WebApi.Security.Records;
 7using Microsoft.AspNetCore.Http;
 8using Microsoft.Extensions.DependencyInjection;
 9
 10namespace ArturRios.Util.WebApi.Security.Authentication;
 11
 12/// <summary>Validates the app's own HMAC-signed JWT and resolves the user through the registered
 13/// <see cref="IAuthenticatedUserMapper"/> — from claims alone or by an <see cref="IAuthenticationProvider"/>
 14/// lookup, per <see cref="AuthenticationOptions.JwtMode"/>. The token's issuer and audience are checked against
 15/// <see cref="JwtConfiguration.Issuer"/> and <see cref="JwtConfiguration.Audience"/> only when
 16/// <see cref="AuthenticationOptions.ValidateIssuer"/> / <see cref="AuthenticationOptions.ValidateAudience"/> are on.</s
 17public class JwtTokenValidator : ITokenValidator
 18{
 119    private static readonly JwtSecurityTokenHandler TokenReader = new();
 20
 21    private readonly JwtConfiguration _jwtConfig;
 22    private readonly JwtHandler _jwtHandler;
 23    private readonly IAuthenticatedUserMapper _mapper;
 24    private readonly AuthenticationOptions _options;
 25
 26    /// <summary>Creates the validator.</summary>
 27    /// <param name="jwtConfig">Provides the key material used to validate the token, and the expected issuer and audien
 28    /// <param name="jwtHandler">Validates token signatures.</param>
 29    /// <param name="mapper">Interprets the token's claims as the app's user.</param>
 30    /// <param name="options">Controls the issuer/audience checks and how the user is resolved once the signature is val
 31    /// <exception cref="InvalidOperationException"><see cref="AuthenticationOptions.ValidateIssuer"/> is on and
 32    /// <see cref="JwtConfiguration.Issuer"/> is blank, or <see cref="AuthenticationOptions.ValidateAudience"/> is on an
 33    /// <see cref="JwtConfiguration.Audience"/> is blank.</exception>
 5734    public JwtTokenValidator(
 5735        JwtConfiguration jwtConfig,
 5736        JwtHandler jwtHandler,
 5737        IAuthenticatedUserMapper mapper,
 5738        AuthenticationOptions options)
 39    {
 40        // Checked here because the validator is created when AuthenticationMiddleware is built, at application
 41        // startup. A check that is on with nothing to compare against would otherwise reject every token at run
 42        // time instead of failing the misconfigured deployment.
 5743        EnsureExpectedValuesConfigured(jwtConfig, options);
 44
 5145        _jwtConfig = jwtConfig;
 5146        _jwtHandler = jwtHandler;
 5147        _mapper = mapper;
 5148        _options = options;
 5149    }
 50
 51    /// <inheritdoc />
 52    public async Task<TokenValidationResult> ValidateAsync(string token, HttpContext context)
 53    {
 54        // Every configured key when there are any, so a token signed with a key that is no longer
 55        // the signing key stays valid until it is withdrawn — which is what makes replacing a signing
 56        // secret a rotation rather than a cutover that invalidates every token in flight. Falls back
 57        // to the single secret when no keys are configured, which is what every configuration written
 58        // before ArturRios.Jwt 1.1.0 looks like.
 4459        var isValid = _jwtConfig.Keys.Count > 0
 4460            ? await _jwtHandler.IsTokenValidAsync(token, _jwtConfig.Keys)
 4461            : await _jwtHandler.IsTokenValidAsync(token, _jwtConfig.Secret);
 62
 4463        if (!isValid)
 64        {
 1065            return new TokenValidationResult(null, "Invalid token");
 66        }
 67
 68        // Only after the signature: before it, the claims are whatever the sender wrote.
 3469        var issuerAndAudienceError = CheckIssuerAndAudience(token);
 70
 3471        if (issuerAndAudienceError is not null)
 72        {
 973            return new TokenValidationResult(null, issuerAndAudienceError);
 74        }
 75
 2576        var claims = TokenClaimsReader.Read(token);
 77
 2578        if (claims is null)
 79        {
 080            return new TokenValidationResult(null, "Could not read token claims");
 81        }
 82
 2583        if (_options.JwtMode == JwtValidationMode.ClaimsOnly)
 84        {
 2085            var claimsUser = MapUser(claims);
 86
 2087            return new TokenValidationResult(claimsUser, claimsUser is null ? "Could not retrieve user from token" : nul
 88        }
 89
 590        var userId = MapId(claims);
 91
 592        if (userId is null)
 93        {
 294            return new TokenValidationResult(null, "Could not retrieve user id from token");
 95        }
 96
 397        var provider = context.RequestServices.GetRequiredService<IAuthenticationProvider>();
 398        var user = provider.GetAuthenticatedUserById(userId.Value);
 99
 3100        return new TokenValidationResult(user, user is null ? "User not found" : null);
 44101    }
 102
 103    private string? CheckIssuerAndAudience(string token)
 104    {
 34105        if (!_options.ValidateIssuer && !_options.ValidateAudience)
 106        {
 21107            return null;
 108        }
 109
 110        JwtSecurityToken jwt;
 111
 112        try
 113        {
 13114            jwt = TokenReader.ReadJwtToken(token);
 13115        }
 0116        catch (Exception)
 117        {
 0118            return "Could not read token claims";
 119        }
 120
 13121        if (_options.ValidateIssuer && !string.Equals(jwt.Issuer, _jwtConfig.Issuer, StringComparison.Ordinal))
 122        {
 5123            return "Invalid token issuer";
 124        }
 125
 8126        if (_options.ValidateAudience && !jwt.Audiences.Contains(_jwtConfig.Audience, StringComparer.Ordinal))
 127        {
 4128            return "Invalid token audience";
 129        }
 130
 4131        return null;
 0132    }
 133
 134    private static void EnsureExpectedValuesConfigured(JwtConfiguration jwtConfig, AuthenticationOptions options)
 135    {
 57136        if (options.ValidateIssuer && string.IsNullOrWhiteSpace(jwtConfig.Issuer))
 137        {
 3138            throw new InvalidOperationException(
 3139                "AuthenticationOptions.ValidateIssuer is on, but JwtConfiguration.Issuer is empty. Configure the " +
 3140                "issuer the app's tokens are minted with, or turn ValidateIssuer off.");
 141        }
 142
 54143        if (options.ValidateAudience && string.IsNullOrWhiteSpace(jwtConfig.Audience))
 144        {
 3145            throw new InvalidOperationException(
 3146                "AuthenticationOptions.ValidateAudience is on, but JwtConfiguration.Audience is empty. Configure " +
 3147                "the audience the app's tokens are minted for, or turn ValidateAudience off.");
 148        }
 51149    }
 150
 151    private IAuthenticatedUser? MapUser(IReadOnlyDictionary<string, string> claims)
 152    {
 153        try
 154        {
 20155            return _mapper.FromClaims(claims);
 156        }
 1157        catch (Exception)
 158        {
 1159            return null;
 160        }
 20161    }
 162
 163    private Guid? MapId(IReadOnlyDictionary<string, string> claims)
 164    {
 165        try
 166        {
 5167            return _mapper.IdFromClaims(claims);
 168        }
 1169        catch (Exception)
 170        {
 1171            return null;
 172        }
 5173    }
 174}