< Summary

Information
Class: Elsa.ExternalAuthentication.Services.PreviewInitiationResult
Assembly: Elsa.ExternalAuthentication
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.ExternalAuthentication/Services/PreviewSignInService.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 152
Line coverage: 0%
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%210%

File(s)

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

#LineLine coverage
 1using System.Security.Claims;
 2using System.Security.Cryptography;
 3using System.Text;
 4using Elsa.Common;
 5using Elsa.ExternalAuthentication.Contracts;
 6using Elsa.ExternalAuthentication.Models;
 7using Elsa.ExternalAuthentication.Notifications;
 8using Elsa.ExternalAuthentication.Options;
 9using Microsoft.AspNetCore.DataProtection;
 10using Microsoft.Extensions.Options;
 11
 12namespace Elsa.ExternalAuthentication.Services;
 13
 14/// <summary>Executes an administrator-bound adapter flow without resolving links, provisioning users, sessions, or cred
 15public sealed class PreviewSignInService(
 16    IdentityProviderConnectionManagementService management,
 17    IExternalAuthenticationAdapterRegistry adapters,
 18    IEnumerable<ISecretBindingResolver> secretBindingResolvers,
 19    IEnumerable<IUnlinkedIdentityPolicy> policies,
 20    IExternalIdentityProvisioner provisioner,
 21    IPermissionGrantResolver permissionGrants,
 22    IExternalAuthenticationStateStore stateStore,
 23    IPreviewResultStore results,
 24    IExternalAuthenticationHandleHasher handles,
 25    IDataProtectionProvider dataProtection,
 26    ISystemClock clock,
 27    IOptions<ExternalAuthenticationOptions> options,
 28    ExternalAuthenticationSecurityNotifier notifier)
 29{
 30    private const string StartPurpose = "PreviewStart";
 31    private readonly IReadOnlyDictionary<string, ISecretBindingResolver> _resolvers = secretBindingResolvers.ToDictionar
 32    private readonly IReadOnlyDictionary<string, IUnlinkedIdentityPolicy> _policies = policies.ToDictionary(x => x.Type,
 33
 34    public async ValueTask<PreviewInitiationResult> InitiateAsync(string connectionId, long expectedRevision, string ten
 35    {
 36        var lookup = await management.FindAsync(connectionId, tenantId, cancellationToken);
 37        if (lookup is not ManagementConnectionLookupResult.Found(var connection)) return new PreviewInitiationResult.Not
 38        if (connection.Connection.Revision != expectedRevision) return new PreviewInitiationResult.PreconditionFailed(co
 39        var adminId = ActorId(administrator);
 40        if (adminId is null) return new PreviewInitiationResult.Forbidden();
 41        var handle = Opaque();
 42        var transaction = new BrokerTransaction
 43        {
 44            HandleHash = handles.Hash(handle), Purpose = BrokerTransactionPurpose.Preview, ClientId = adminId,
 45            CallbackUri = new Uri($"/external-authentication/previews/{Uri.EscapeDataString(handle)}/authorize", UriKind
 46            ReturnPath = "/", TenantId = tenantId, ConnectionId = connection.Connection.Id,
 47            ConnectionMaterialRevision = connection.Connection.MaterialRevision, PkceChallenge = string.Empty,
 48            ExpiresAt = clock.UtcNow.Add(options.Value.Lifetimes.PreviewLifetime)
 49        };
 50        await stateStore.PutAsync(StartPurpose, transaction.HandleHash, transaction, transaction.ExpiresAt, cancellation
 51        return new PreviewInitiationResult.Started(handle, transaction.ExpiresAt);
 52    }
 53
 54    /// <summary>
 55    /// Continues a preview from the opaque, one-time browser handle created by an authorized administrator.
 56    /// The browser navigation is anonymous because it cannot carry Studio's bearer credential.
 57    /// </summary>
 58    public async ValueTask<PreviewAuthorizeResult> AuthorizeAsync(string previewHandle, CancellationToken cancellationTo
 59    {
 60        var taken = await stateStore.TryTakeAsync<BrokerTransaction>(StartPurpose, handles.Hash(previewHandle), cancella
 61        if (taken is not TakeResult<BrokerTransaction>.Taken { Value: var transaction } || string.IsNullOrWhiteSpace(tra
 62            return new PreviewAuthorizeResult.Invalid();
 63        var lookup = await management.FindAsync(transaction.ConnectionId!, transaction.TenantId, cancellationToken);
 64        if (lookup is not ManagementConnectionLookupResult.Found(var connection) || !string.Equals(connection.Connection
 65            return new PreviewAuthorizeResult.Invalid();
 66
 67        var state = Opaque();
 68        transaction.HandleHash = handles.Hash(state);
 69        transaction.ClientState = previewHandle;
 70        var secrets = await ResolveSecretsAsync(connection.Connection.SecretBindings, cancellationToken);
 71        try
 72        {
 73            transaction.SecretGenerationFingerprint = SecretFingerprint(secrets);
 74            var request = await adapter.CreateAuthorizationRequestAsync(new ExternalAuthorizationContext(connection, sec
 75            transaction.ProtectedPayload = dataProtection.CreateProtector("Elsa.ExternalAuthentication.AdapterPayload.v1
 76            await stateStore.PutAsync(BrokerTransactionPurpose.Preview.ToString(), transaction.HandleHash, transaction, 
 77            return new PreviewAuthorizeResult.Redirect(request.NavigationUri);
 78        }
 79        finally { Dispose(secrets); }
 80    }
 81
 82    public async ValueTask<PreviewCallbackResult> CompleteAsync(string connectionId, string state, IReadOnlyDictionary<s
 83    {
 84        var taken = await stateStore.TryTakeAsync<BrokerTransaction>(BrokerTransactionPurpose.Preview.ToString(), handle
 85        if (taken is not TakeResult<BrokerTransaction>.Taken { Value: var transaction } || !string.Equals(transaction.Co
 86            return new PreviewCallbackResult.Invalid();
 87        var lookup = await management.FindAsync(connectionId, transaction.TenantId, cancellationToken);
 88        if (lookup is not ManagementConnectionLookupResult.Found(var connection) || !string.Equals(connection.Connection
 89            return new PreviewCallbackResult.Invalid();
 90        var secrets = await ResolveSecretsAsync(connection.Connection.SecretBindings, cancellationToken);
 91        try
 92        {
 93            if (!string.Equals(transaction.SecretGenerationFingerprint, SecretFingerprint(secrets), StringComparison.Ord
 94            var protectedPayload = transaction.ProtectedPayload;
 95            transaction.ProtectedPayload = dataProtection.CreateProtector("Elsa.ExternalAuthentication.AdapterPayload.v1
 96            ExternalAuthenticationResult authentication;
 97            try { authentication = await adapter.AuthenticateCallbackAsync(new ExternalCallbackContext(connection, secre
 98            finally { transaction.ProtectedPayload = protectedPayload; }
 99            var existingLink = await provisioner.FindLinkAsync(transaction.TenantId, ConnectionRevisionCalculator.Normal
 100            var decision = existingLink is null ? await DescribePolicyAsync(connection, authentication, cancellationToke
 101            var grants = existingLink is null
 102                ? new PermissionGrantResult([], [new PermissionGrantWarning("user_resolution_required", "Permission gran
 103                : await permissionGrants.ResolveAsync(new PermissionGrantResolutionContext(transaction.TenantId, existin
 104            var result = new PreviewResult(handles.Hash(transaction.ClientState), transaction.ClientId, transaction.Tena
 105                authentication.Identity.Issuer, Mask(authentication.Identity.Subject), ExternalAuthenticationRedactor.Re
 106            await results.SaveAsync(result, cancellationToken);
 107            await notifier.PublishAsync(new IdentityProviderConnectionPreviewed(ExternalAuthenticationSecurityNotifier.C
 108            return new PreviewCallbackResult.Completed();
 109        }
 110        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
 111        catch { return new PreviewCallbackResult.Invalid(); }
 112        finally { Dispose(secrets); }
 113    }
 114
 115    public async ValueTask<TakeResult<PreviewResult>> TakeResultAsync(string previewHandle, string tenantId, ClaimsPrinc
 116    {
 117        var adminId = ActorId(administrator);
 118        if (adminId is null) return new TakeResult<PreviewResult>.NotFound();
 119        var result = await results.TryTakeAsync(handles.Hash(previewHandle), adminId, cancellationToken);
 120        return result is TakeResult<PreviewResult>.Taken { Value: var preview } && !string.Equals(preview.TenantId, tena
 121    }
 122
 123    private async ValueTask<string> DescribePolicyAsync(EffectiveIdentityProviderConnection connection, ExternalAuthenti
 124    {
 125        var selection = connection.Connection.UnlinkedPolicy ?? new PolicySelection(options.Value.UnlinkedIdentityPolicy
 126        if (!_policies.TryGetValue(selection.Type, out var policy)) return "unlinked_policy_unavailable";
 127        var decision = await policy.EvaluateAsync(new UnlinkedIdentityContext(connection.Connection.TenantId, connection
 128        return decision switch
 129        {
 130            UnlinkedIdentityDecision.CreateUser => "would_create_user_and_link",
 131            UnlinkedIdentityDecision.LinkExistingUser => "would_link_existing_user",
 132            UnlinkedIdentityDecision.Reject => "would_reject_unlinked_identity",
 133            _ => "unlinked_policy_unknown"
 134        };
 135    }
 136
 137    private async ValueTask<IReadOnlyDictionary<string, ResolvedSecretBinding>> ResolveSecretsAsync(IDictionary<string, 
 138    {
 139        var values = new Dictionary<string, ResolvedSecretBinding>(StringComparer.Ordinal);
 140        try { foreach (var (name, binding) in bindings) values[name] = await (_resolvers.TryGetValue(binding.ResolverTyp
 141        catch { Dispose(values); throw; }
 142    }
 143    private static string? ActorId(ClaimsPrincipal user) => user.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? user.Fin
 144    private static string Opaque() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(32)).TrimEnd('=').Replace('+
 145    private static string Mask(string subject) => subject.Length <= 4 ? "****" : subject[..2] + "***" + subject[^2..];
 146    private static string SecretFingerprint(IReadOnlyDictionary<string, ResolvedSecretBinding> values) => Convert.ToHexS
 147    private static void Dispose(IReadOnlyDictionary<string, ResolvedSecretBinding> values) { foreach (var value in value
 148}
 149
 0150public abstract record PreviewInitiationResult { private PreviewInitiationResult() { } public sealed record Started(stri
 151public abstract record PreviewAuthorizeResult { private PreviewAuthorizeResult() { } public sealed record Redirect(Uri N
 152public abstract record PreviewCallbackResult { private PreviewCallbackResult() { } public sealed record Completed : Prev

Methods/Properties

.ctor()