< Summary

Information
Class: Elsa.ExternalAuthentication.Services.DefaultExternalIdentityResolver
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalIdentityResolver.cs
Line coverage
93%
Covered lines: 43
Uncovered lines: 3
Coverable lines: 46
Total lines: 94
Line coverage: 93.4%
Branch coverage
79%
Covered branches: 19
Total branches: 24
Branch coverage: 79.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
ResolveAsync()60%101088.88%
RecordSuccessfulSignInAsync(...)50%22100%
GetPolicySelection(...)100%44100%
ValidateLink(...)100%88100%

File(s)

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

#LineLine coverage
 1using Elsa.ExternalAuthentication.Contracts;
 2using Elsa.ExternalAuthentication.Models;
 3using Elsa.ExternalAuthentication.Options;
 4using Microsoft.Extensions.Options;
 5
 6namespace Elsa.ExternalAuthentication.Services;
 7
 8/// <summary>
 9/// Resolves normalized external identities to Elsa users and delegates tuple uniqueness to the atomic provisioner.
 10/// </summary>
 911public sealed class DefaultExternalIdentityResolver(
 912    IExternalIdentityProvisioner provisioner,
 913    IEnumerable<IUnlinkedIdentityPolicy> policies,
 914    IOptions<ExternalAuthenticationOptions> options) : IExternalIdentityResolver, IExternalIdentitySignInTracker
 15{
 1816    private readonly IReadOnlyDictionary<string, IUnlinkedIdentityPolicy> _policies = policies.ToDictionary(x => x.Type,
 17
 18    public async ValueTask<ExternalIdentityResolution> ResolveAsync(ExternalIdentityResolutionContext context, Cancellat
 19    {
 2420        ArgumentNullException.ThrowIfNull(context);
 21
 2422        var connectionKey = ConnectionRevisionCalculator.NormalizeKey(context.Connection.Connection.Key);
 2423        var existingLink = await provisioner.FindLinkAsync(context.TargetTenantId, connectionKey, context.Identity, canc
 2424        if (existingLink is not null)
 25        {
 2026            ValidateLink(existingLink, context);
 1927            return new ExternalIdentityResolution(existingLink.UserId, false);
 28        }
 29
 430        var selection = GetPolicySelection(context.Connection);
 431        var policy = _policies.GetValueOrDefault(selection.Type)
 432            ?? throw new InvalidOperationException($"The unlinked identity policy '{selection.Type}' is not available.")
 433        var decision = await policy.EvaluateAsync(new UnlinkedIdentityContext(
 434            context.TargetTenantId,
 435            context.Connection,
 436            context.Identity,
 437            context.ProjectedClaims,
 438            selection.Settings), cancellationToken);
 39
 440        var result = decision switch
 441        {
 242            UnlinkedIdentityDecision.Reject reject => throw new ExternalIdentityUnlinkedException(reject.SafeReason),
 243            UnlinkedIdentityDecision.CreateUser createUser => await provisioner.CreateLinkOrGetExistingAsync(
 244                new ProvisioningRequest(context.TargetTenantId, connectionKey, context.Identity, createUser.Proposal), c
 045            UnlinkedIdentityDecision.LinkExistingUser linkExistingUser => await provisioner.CreateLinkOrGetExistingAsync
 046                new ProvisioningRequest(context.TargetTenantId, connectionKey, context.Identity, null, linkExistingUser.
 047            _ => throw new InvalidOperationException("The unlinked identity policy returned an unsupported decision.")
 448        };
 49
 250        ValidateLink(result.Link, context);
 251        return new ExternalIdentityResolution(result.UserId, result.WasCreated);
 2152    }
 53
 54    public ValueTask<bool> RecordSuccessfulSignInAsync(
 55        string tenantId,
 56        string connectionKey,
 57        ExternalIdentity identity,
 58        string userId,
 59        DateTimeOffset signedInAt,
 60        CancellationToken cancellationToken = default) =>
 461        provisioner is IExternalIdentitySignInTracker tracker
 462            ? tracker.RecordSuccessfulSignInAsync(tenantId, connectionKey, identity, userId, signedInAt, cancellationTok
 463            : ValueTask.FromResult(true);
 64
 65    private PolicySelection GetPolicySelection(EffectiveIdentityProviderConnection connection)
 66    {
 467        var configuredPolicy = connection.Connection.UnlinkedPolicy;
 468        var mayOverride = connection.Ownership == ConnectionSourceOwnership.Configuration || options.Value.UnlinkedIdent
 469        if (configuredPolicy is not null && mayOverride)
 270            return configuredPolicy;
 71
 272        return new PolicySelection(options.Value.UnlinkedIdentityPolicy.DefaultType, 1, default);
 73    }
 74
 75    private static void ValidateLink(ExternalIdentityLink link, ExternalIdentityResolutionContext context)
 76    {
 2277        if (!string.Equals(link.TenantId, context.TargetTenantId, StringComparison.Ordinal) ||
 2278            !string.Equals(link.ConnectionKey, ConnectionRevisionCalculator.NormalizeKey(context.Connection.Connection.K
 2279            !string.Equals(link.Issuer, context.Identity.Issuer, StringComparison.Ordinal) ||
 2280            string.IsNullOrWhiteSpace(link.UserId))
 81        {
 182            throw new InvalidOperationException("The external identity provisioner returned a link outside the requested
 83        }
 2184    }
 85}
 86
 87/// <summary>
 88/// Signals that a successfully authenticated external identity has no permitted Elsa user link.
 89/// Endpoint code must translate this exception to the safe <c>identity_unlinked</c> broker category.
 90/// </summary>
 91public sealed class ExternalIdentityUnlinkedException(string safeReason) : InvalidOperationException
 92{
 93    public string SafeReason { get; } = safeReason;
 94}