< 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
83%
Covered lines: 51
Uncovered lines: 10
Coverable lines: 61
Total lines: 90
Line coverage: 83.6%
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.Identity.Models;
 10
 11namespace Elsa.ExternalAuthentication.Services;
 12
 213public sealed class DefaultExternalAuthenticationTokenIssuer(
 214    IExternalAuthenticationSessionStore sessionStore,
 215    IIdentityProviderConnectionRegistry connectionRegistry,
 216    IEnumerable<ISecretBindingResolver> secretBindingResolvers,
 217    IUserProvider userProvider,
 218    IRoleProvider roleProvider,
 219    IElsaTokenService tokenService,
 220    ITenantAccessor tenantAccessor,
 221    ISystemClock clock) : IExternalAuthenticationTokenIssuer
 22{
 23    public async ValueTask<ExternalTokenResponse> IssueAsync(ExternalAuthenticationSession session, CancellationToken ca
 24    {
 225        var refreshToken = CreateRefreshToken(session.Id);
 226        session.CurrentRefreshTokenHash = Hash(refreshToken);
 227        await sessionStore.SaveAsync(session, cancellationToken);
 228        return await IssueResponseAsync(session, refreshToken, cancellationToken);
 229    }
 30
 31    public async ValueTask<ExternalTokenResponse> RefreshAsync(string clientId, SensitiveString refreshToken, Cancellati
 32    {
 333        var rawToken = refreshToken.Reveal();
 334        var separator = rawToken.IndexOf('.', StringComparison.Ordinal);
 335        if (separator <= 0 || separator == rawToken.Length - 1)
 036            throw new InvalidOperationException("The external refresh token is invalid.");
 337        var sessionId = rawToken[..separator];
 338        var currentHash = Hash(rawToken);
 339        var session = await sessionStore.FindByIdAsync(sessionId, cancellationToken);
 340        if (session is null || !string.Equals(session.AuthenticationClientId, clientId, StringComparison.Ordinal))
 041            throw new InvalidOperationException("The external refresh token is invalid.");
 342        var connection = await connectionRegistry.FindByKeyAsync(session.TenantId, session.ConnectionKey, cancellationTo
 343        if (session.RevokedAt != null || session.ExpiresAt <= clock.UtcNow || connection is null || connection.IsShadowe
 044            throw new InvalidOperationException("The external authentication session is no longer valid.");
 345        if (!string.Equals(session.SecretGenerationFingerprint, await GetSecretFingerprintAsync(connection.Connection.Se
 046            throw new InvalidOperationException("The external authentication session secrets changed.");
 47
 348        var nextToken = CreateRefreshToken(session.Id);
 349        var rotation = await sessionStore.TryRotateRefreshTokenAsync(session.Id, currentHash, session.RefreshGeneration,
 350        if (rotation is not ExternalAuthenticationSessionRotationResult.Rotated { Session: var rotated })
 151            throw new InvalidOperationException("The external refresh token cannot be used.");
 52
 253        return await IssueResponseAsync(rotated, nextToken, cancellationToken);
 254    }
 55
 56    private async ValueTask<ExternalTokenResponse> IssueResponseAsync(ExternalAuthenticationSession session, string refr
 57    {
 458        using var tenantContext = tenantAccessor.PushContext(new Tenant { Id = session.TenantId, Name = session.TenantId
 459        var user = await userProvider.FindAsync(new UserFilter { Id = session.UserId }, cancellationToken)
 460            ?? throw new InvalidOperationException("The external authentication session user no longer exists.");
 461        var roles = (await roleProvider.FindByIdsAsync(user.Roles, cancellationToken)).ToArray();
 862        var permissions = roles.SelectMany(x => x.Permissions).Concat(session.ExternalGrants.Select(x => x.Permission)).
 663        var accessToken = await tokenService.IssueAccessTokenAsync(new TokenIssuanceContext(user, roles.Select(x => x.Na
 464        var now = clock.UtcNow;
 465        return new ExternalTokenResponse(
 466            accessToken.Token,
 467            "Bearer",
 468            Math.Max(0, (long)(accessToken.ExpiresAt - now).TotalSeconds),
 469            refreshToken,
 470            Math.Max(0, (long)(session.RefreshExpiresAt - now).TotalSeconds),
 471            Math.Max(0, (long)(session.ExpiresAt - now).TotalSeconds));
 472    }
 73
 574    private static string CreateRefreshToken(string sessionId) => $"{sessionId}.{Convert.ToBase64String(RandomNumberGene
 875    private static string Hash(string token) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(token)));
 76
 77    private async ValueTask<string> GetSecretFingerprintAsync(IDictionary<string, SecretBinding> bindings, CancellationT
 78    {
 379        var fingerprints = new List<string>();
 680        foreach (var (name, binding) in bindings)
 81        {
 082            var resolver = secretBindingResolvers.FirstOrDefault(x => string.Equals(x.Type, binding.ResolverType, String
 083                ?? throw new InvalidOperationException("A required secret binding resolver is unavailable.");
 084            var resolved = await resolver.ResolveAsync(binding, cancellationToken);
 085            fingerprints.Add($"{name}:{resolved.GenerationFingerprint}");
 086            resolved.Value.Dispose();
 087        }
 388        return Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(string.Join("\n", fingerprints.OrderBy(x => x,
 389    }
 90}