< Summary

Information
Class: Elsa.ExternalAuthentication.Services.DefaultExternalAuthenticationTokenIssuer
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs
Line coverage
85%
Covered lines: 59
Uncovered lines: 10
Coverable lines: 69
Total lines: 107
Line coverage: 85.5%
Branch coverage
53%
Covered branches: 16
Total branches: 30
Branch coverage: 53.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
IssueAsync()100%11100%
RefreshAsync()53.84%312680%
IssueResponseAsync()50%22100%
CreateRefreshToken(...)100%11100%
Hash(...)100%11100%
GetSecretFingerprintAsync()25%8437.5%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalAuthenticationTokenIssuer.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using Elsa.Common;
 4using Elsa.Common.Multitenancy;
 5using Elsa.Extensions;
 6using Elsa.ExternalAuthentication.Contracts;
 7using Elsa.ExternalAuthentication.Models;
 8using Elsa.Identity.Contracts;
 9using Elsa.ExternalAuthentication.Options;
 10using Elsa.Identity.Models;
 11using Microsoft.Extensions.Options;
 12
 13namespace Elsa.ExternalAuthentication.Services;
 14
 415public sealed class DefaultExternalAuthenticationTokenIssuer(
 416    IExternalAuthenticationSessionStore sessionStore,
 417    IIdentityProviderConnectionRegistry connectionRegistry,
 418    IEnumerable<ISecretBindingResolver> secretBindingResolvers,
 419    IUserProvider userProvider,
 420    IRoleProvider roleProvider,
 421    IElsaTokenService tokenService,
 422    ITenantAccessor tenantAccessor,
 423    ISystemClock clock,
 424    IOptions<ExternalAuthenticationOptions> options) : IExternalAuthenticationTokenIssuer
 25{
 26    public async ValueTask<ExternalTokenResponse> IssueAsync(ExternalAuthenticationSession session, CancellationToken ca
 27    {
 428        var refreshToken = CreateRefreshToken(session.Id);
 429        session.CurrentRefreshTokenHash = Hash(refreshToken);
 430        await sessionStore.SaveAsync(session, cancellationToken);
 431        return await IssueResponseAsync(session, refreshToken, cancellationToken);
 432    }
 33
 34    public async ValueTask<ExternalTokenResponse> RefreshAsync(string clientId, SensitiveString refreshToken, Cancellati
 35    {
 336        var rawToken = refreshToken.Reveal();
 337        var separator = rawToken.IndexOf('.', StringComparison.Ordinal);
 338        if (separator <= 0 || separator == rawToken.Length - 1)
 039            throw new InvalidOperationException("The external refresh token is invalid.");
 340        var sessionId = rawToken[..separator];
 341        var currentHash = Hash(rawToken);
 342        var session = await sessionStore.FindByIdAsync(sessionId, cancellationToken);
 343        if (session is null || !string.Equals(session.AuthenticationClientId, clientId, StringComparison.Ordinal))
 044            throw new InvalidOperationException("The external refresh token is invalid.");
 345        var connection = await connectionRegistry.FindByKeyAsync(session.TenantId, session.ConnectionKey, cancellationTo
 346        if (session.RevokedAt != null || session.ExpiresAt <= clock.UtcNow || connection is null || connection.IsShadowe
 047            throw new InvalidOperationException("The external authentication session is no longer valid.");
 348        if (!string.Equals(session.SecretGenerationFingerprint, await GetSecretFingerprintAsync(connection.Connection.Se
 049            throw new InvalidOperationException("The external authentication session secrets changed.");
 50
 351        var nextToken = CreateRefreshToken(session.Id);
 352        var rotation = await sessionStore.TryRotateRefreshTokenAsync(session.Id, currentHash, session.RefreshGeneration,
 353        if (rotation is not ExternalAuthenticationSessionRotationResult.Rotated { Session: var rotated })
 154            throw new InvalidOperationException("The external refresh token cannot be used.");
 55
 256        return await IssueResponseAsync(rotated, nextToken, cancellationToken);
 257    }
 58
 59    private async ValueTask<ExternalTokenResponse> IssueResponseAsync(ExternalAuthenticationSession session, string refr
 60    {
 661        using var tenantContext = tenantAccessor.PushContext(new()
 662            { Id = session.TenantId, Name = session.TenantId });
 663        var user = await userProvider.FindAsync(new()
 664                       { Id = session.UserId }, cancellationToken)
 665            ?? throw new InvalidOperationException("The external authentication session user no longer exists.");
 666        var roles = (await roleProvider.FindByIdsAsync(user.Roles, cancellationToken)).ToArray();
 67        // Role permissions go through the same deployment boundary as the external grants beside them. They
 68        // used to be concatenated raw, which let a permission the boundary had just excluded during grant
 69        // resolution reappear here from the same roles -- making the deny list unenforceable for anything a
 70        // role happened to carry, and ElsaRolePermissionGrantSource's own filtering pointless. Re-applying it
 71        // at issuance also picks up a boundary that changed since sign-in, because refreshing reissues.
 72        // With no boundary configured, which is the default, every well-formed permission passes and nothing
 73        // about this changes.
 674        var boundary = new PermissionGrantBoundary(options.Value.PermissionGrants);
 1075        var permissions = roles.SelectMany(x => x.Permissions)
 476            .Concat(session.ExternalGrants.Select(x => x.Permission))
 677            .Where(boundary.Allows)
 678            .Distinct(StringComparer.Ordinal)
 679            .ToArray();
 1080        var accessToken = await tokenService.IssueAccessTokenAsync(new(user, roles.Select(x => x.Name).ToArray(), permis
 681        var now = clock.UtcNow;
 682        return new(
 683            accessToken.Token,
 684            "Bearer",
 685            Math.Max(0, (long)(accessToken.ExpiresAt - now).TotalSeconds),
 686            refreshToken,
 687            Math.Max(0, (long)(session.RefreshExpiresAt - now).TotalSeconds),
 688            Math.Max(0, (long)(session.ExpiresAt - now).TotalSeconds));
 689    }
 90
 791    private static string CreateRefreshToken(string sessionId) => $"{sessionId}.{Convert.ToBase64String(RandomNumberGene
 1092    private static string Hash(string token) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
 93
 94    private async ValueTask<string> GetSecretFingerprintAsync(IDictionary<string, SecretBinding> bindings, CancellationT
 95    {
 396        var fingerprints = new List<string>();
 697        foreach (var (name, binding) in bindings)
 98        {
 099            var resolver = secretBindingResolvers.FirstOrDefault(x => string.Equals(x.Type, binding.ResolverType, String
 0100                ?? throw new InvalidOperationException("A required secret binding resolver is unavailable.");
 0101            var resolved = await resolver.ResolveAsync(binding, cancellationToken);
 0102            fingerprints.Add($"{name}:{resolved.GenerationFingerprint}");
 0103            resolved.Value.Dispose();
 0104        }
 3105        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n", fingerprints.OrderBy(x => x,
 3106    }
 107}