< Summary

Information
Class: ProjectTemplate.Web.Authentication.Claims.ApplicationClaimsTransformation
Assembly: ProjectTemplate.Web
File(s): /home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Authentication/Claims/ApplicationClaimsTransformation.cs
Line coverage
98%
Covered lines: 67
Uncovered lines: 1
Coverable lines: 68
Total lines: 153
Line coverage: 98.5%
Branch coverage
84%
Covered branches: 27
Total branches: 32
Branch coverage: 84.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)50%22100%
TransformAsync(...)100%44100%
WithNormalizedNameAndRoleClaimTypes(...)75%44100%
ResolveClaimType(...)75%44100%
ResolveMappings(...)75%44100%
NormalizeClaim(...)92.85%141495.83%
HasClaim(...)100%11100%

File(s)

/home/runner/work/NetCoreApplicationTemplate/NetCoreApplicationTemplate/src/ProjectTemplate.Web/Authentication/Claims/ApplicationClaimsTransformation.cs

#LineLine coverage
 1using System.Security.Claims;
 2using Microsoft.AspNetCore.Authentication;
 3using Microsoft.Extensions.Options;
 4using ProjectTemplate.Web.Authentication.Options;
 5
 6namespace ProjectTemplate.Web.Authentication.Claims;
 7
 8/// <summary>
 9/// Normalizes provider-specific claims into application-owned claim names.
 10/// </summary>
 11/// <param name="authenticationOptionsAccessor">The application authentication options accessor.</param>
 9612public sealed class ApplicationClaimsTransformation(
 9613    IOptions<ApplicationAuthenticationOptions> authenticationOptionsAccessor) : IClaimsTransformation
 14{
 9615    private readonly IOptions<ApplicationAuthenticationOptions> _authenticationOptionsAccessor =
 9616        authenticationOptionsAccessor ?? throw new ArgumentNullException(nameof(authenticationOptionsAccessor));
 17
 18    /// <inheritdoc />
 19    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
 20    {
 1921        ArgumentNullException.ThrowIfNull(principal);
 22
 1923        ApplicationClaimsTransformationOptions options =
 1924            _authenticationOptionsAccessor.Value.ClaimsTransformation;
 25
 1926        if (!options.Enabled)
 27        {
 128            return Task.FromResult(principal);
 29        }
 30
 31        // Transformation may run more than once per request, and the incoming principal can be shared with other
 32        // components, so normalize copies instead of editing the caller's principal in place.
 1833        var transformed = new ClaimsPrincipal();
 34
 7235        foreach (ClaimsIdentity source in principal.Identities)
 36        {
 1837            ClaimsIdentity identity = source.Clone();
 1838            ApplicationClaimMappingOptions mappings = ResolveMappings(options, identity.AuthenticationType);
 39
 1840            NormalizeClaim(identity, ApplicationClaimTypes.Subject, mappings.Subject, options.RemoveOriginalClaims);
 1841            NormalizeClaim(identity, ApplicationClaimTypes.Name, mappings.Name, options.RemoveOriginalClaims);
 1842            NormalizeClaim(identity, ApplicationClaimTypes.Email, mappings.Email, options.RemoveOriginalClaims);
 1843            NormalizeClaim(identity, ApplicationClaimTypes.Role, mappings.Role, options.RemoveOriginalClaims);
 1844            NormalizeClaim(identity, ApplicationClaimTypes.Group, mappings.Group, options.RemoveOriginalClaims);
 1845            NormalizeClaim(identity, ApplicationClaimTypes.Permission, mappings.Permission, options.RemoveOriginalClaims
 46
 1847            transformed.AddIdentity(WithNormalizedNameAndRoleClaimTypes(identity));
 48        }
 49
 1850        return Task.FromResult(transformed);
 51    }
 52
 53    // Points Identity.Name, User.IsInRole, and [Authorize(Roles = "...")] at application:name and
 54    // application:role, so they keep working once RemoveOriginalClaims strips the provider claims. The original
 55    // claim type is kept only when the identity still carries it and has no normalized equivalent (for example,
 56    // when the provider's type is not in the configured mappings).
 57    private static ClaimsIdentity WithNormalizedNameAndRoleClaimTypes(ClaimsIdentity identity)
 58    {
 1859        string nameClaimType = ResolveClaimType(identity, ApplicationClaimTypes.Name, identity.NameClaimType);
 1860        string roleClaimType = ResolveClaimType(identity, ApplicationClaimTypes.Role, identity.RoleClaimType);
 61
 1862        return string.Equals(nameClaimType, identity.NameClaimType, StringComparison.Ordinal)
 1863            && string.Equals(roleClaimType, identity.RoleClaimType, StringComparison.Ordinal)
 1864            ? identity
 1865            : new ClaimsIdentity(identity.Claims, identity.AuthenticationType, nameClaimType, roleClaimType)
 1866            {
 1867                Actor = identity.Actor,
 1868                BootstrapContext = identity.BootstrapContext,
 1869                Label = identity.Label
 1870            };
 71    }
 72
 73    private static string ResolveClaimType(ClaimsIdentity identity, string normalizedClaimType, string currentClaimType)
 74    {
 3675        bool hasNormalized = identity.HasClaim(claim =>
 3676            string.Equals(claim.Type, normalizedClaimType, StringComparison.OrdinalIgnoreCase));
 3677        bool hasCurrent = identity.HasClaim(claim =>
 3678            string.Equals(claim.Type, currentClaimType, StringComparison.OrdinalIgnoreCase));
 79
 3680        return hasNormalized || !hasCurrent ? normalizedClaimType : currentClaimType;
 81    }
 82
 83    private static ApplicationClaimMappingOptions ResolveMappings(
 84        ApplicationClaimsTransformationOptions options,
 85        string? authenticationType)
 86    {
 1887        return !string.IsNullOrWhiteSpace(authenticationType)
 1888            && options.ProviderMappings.TryGetValue(authenticationType, out ApplicationClaimMappingOptions? providerMapp
 1889            ? providerMappings
 1890            : options.DefaultMappings;
 91    }
 92
 93    private static void NormalizeClaim(
 94        ClaimsIdentity identity,
 95        string normalizedClaimType,
 96        IEnumerable<string> sourceClaimTypes,
 97        bool removeOriginalClaims)
 98    {
 10899        var sourceTypes = sourceClaimTypes
 108100            .Where(claimType => !string.IsNullOrWhiteSpace(claimType))
 108101            .ToHashSet(StringComparer.OrdinalIgnoreCase);
 102
 108103        if (sourceTypes.Count == 0)
 104        {
 0105            return;
 106        }
 107
 108108        var sourceClaims = identity.Claims
 108109            .Where(claim => sourceTypes.Contains(claim.Type))
 108110            .ToList();
 111
 108112        if (sourceClaims.Count == 0)
 113        {
 64114            return;
 115        }
 116
 176117        foreach (Claim sourceClaim in sourceClaims)
 118        {
 44119            if (!HasClaim(identity, normalizedClaimType, sourceClaim.Value))
 120            {
 43121                identity.AddClaim(new Claim(
 43122                    normalizedClaimType,
 43123                    sourceClaim.Value,
 43124                    sourceClaim.ValueType,
 43125                    sourceClaim.Issuer,
 43126                    sourceClaim.OriginalIssuer));
 127            }
 128        }
 129
 44130        if (!removeOriginalClaims)
 131        {
 39132            return;
 133        }
 134
 20135        foreach (Claim sourceClaim in sourceClaims)
 136        {
 5137            if (!string.Equals(sourceClaim.Type, normalizedClaimType, StringComparison.OrdinalIgnoreCase))
 138            {
 5139                identity.RemoveClaim(sourceClaim);
 140            }
 141        }
 5142    }
 143
 144    private static bool HasClaim(
 145        ClaimsIdentity identity,
 146        string claimType,
 147        string claimValue)
 148    {
 44149        return identity.Claims.Any(claim =>
 44150            string.Equals(claim.Type, claimType, StringComparison.OrdinalIgnoreCase)
 44151            && string.Equals(claim.Value, claimValue, StringComparison.Ordinal));
 152    }
 153}