< Summary

Information
Class: Elsa.ExternalAuthentication.Services.ExternalIdentityUnlinkedException
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalIdentityResolver.cs
Line coverage
100%
Covered lines: 2
Uncovered lines: 0
Coverable lines: 2
Total lines: 94
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_SafeReason()100%11100%

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>
 11public sealed class DefaultExternalIdentityResolver(
 12    IExternalIdentityProvisioner provisioner,
 13    IEnumerable<IUnlinkedIdentityPolicy> policies,
 14    IOptions<ExternalAuthenticationOptions> options) : IExternalIdentityResolver, IExternalIdentitySignInTracker
 15{
 16    private readonly IReadOnlyDictionary<string, IUnlinkedIdentityPolicy> _policies = policies.ToDictionary(x => x.Type,
 17
 18    public async ValueTask<ExternalIdentityResolution> ResolveAsync(ExternalIdentityResolutionContext context, Cancellat
 19    {
 20        ArgumentNullException.ThrowIfNull(context);
 21
 22        var connectionKey = ConnectionRevisionCalculator.NormalizeKey(context.Connection.Connection.Key);
 23        var existingLink = await provisioner.FindLinkAsync(context.TargetTenantId, connectionKey, context.Identity, canc
 24        if (existingLink is not null)
 25        {
 26            ValidateLink(existingLink, context);
 27            return new ExternalIdentityResolution(existingLink.UserId, false);
 28        }
 29
 30        var selection = GetPolicySelection(context.Connection);
 31        var policy = _policies.GetValueOrDefault(selection.Type)
 32            ?? throw new InvalidOperationException($"The unlinked identity policy '{selection.Type}' is not available.")
 33        var decision = await policy.EvaluateAsync(new UnlinkedIdentityContext(
 34            context.TargetTenantId,
 35            context.Connection,
 36            context.Identity,
 37            context.ProjectedClaims,
 38            selection.Settings), cancellationToken);
 39
 40        var result = decision switch
 41        {
 42            UnlinkedIdentityDecision.Reject reject => throw new ExternalIdentityUnlinkedException(reject.SafeReason),
 43            UnlinkedIdentityDecision.CreateUser createUser => await provisioner.CreateLinkOrGetExistingAsync(
 44                new ProvisioningRequest(context.TargetTenantId, connectionKey, context.Identity, createUser.Proposal), c
 45            UnlinkedIdentityDecision.LinkExistingUser linkExistingUser => await provisioner.CreateLinkOrGetExistingAsync
 46                new ProvisioningRequest(context.TargetTenantId, connectionKey, context.Identity, null, linkExistingUser.
 47            _ => throw new InvalidOperationException("The unlinked identity policy returned an unsupported decision.")
 48        };
 49
 50        ValidateLink(result.Link, context);
 51        return new ExternalIdentityResolution(result.UserId, result.WasCreated);
 52    }
 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) =>
 61        provisioner is IExternalIdentitySignInTracker tracker
 62            ? tracker.RecordSuccessfulSignInAsync(tenantId, connectionKey, identity, userId, signedInAt, cancellationTok
 63            : ValueTask.FromResult(true);
 64
 65    private PolicySelection GetPolicySelection(EffectiveIdentityProviderConnection connection)
 66    {
 67        var configuredPolicy = connection.Connection.UnlinkedPolicy;
 68        var mayOverride = connection.Ownership == ConnectionSourceOwnership.Configuration || options.Value.UnlinkedIdent
 69        if (configuredPolicy is not null && mayOverride)
 70            return configuredPolicy;
 71
 72        return new PolicySelection(options.Value.UnlinkedIdentityPolicy.DefaultType, 1, default);
 73    }
 74
 75    private static void ValidateLink(ExternalIdentityLink link, ExternalIdentityResolutionContext context)
 76    {
 77        if (!string.Equals(link.TenantId, context.TargetTenantId, StringComparison.Ordinal) ||
 78            !string.Equals(link.ConnectionKey, ConnectionRevisionCalculator.NormalizeKey(context.Connection.Connection.K
 79            !string.Equals(link.Issuer, context.Identity.Issuer, StringComparison.Ordinal) ||
 80            string.IsNullOrWhiteSpace(link.UserId))
 81        {
 82            throw new InvalidOperationException("The external identity provisioner returned a link outside the requested
 83        }
 84    }
 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>
 291public sealed class ExternalIdentityUnlinkedException(string safeReason) : InvalidOperationException
 92{
 393    public string SafeReason { get; } = safeReason;
 94}