| | | 1 | | using Elsa.Abstractions; |
| | | 2 | | using Elsa.Common.Multitenancy; |
| | | 3 | | using Elsa.ExternalAuthentication.Constants; |
| | | 4 | | using Elsa.ExternalAuthentication.Models; |
| | | 5 | | using Elsa.ExternalAuthentication.Permissions; |
| | | 6 | | using Elsa.ExternalAuthentication.Services; |
| | | 7 | | using Microsoft.AspNetCore.Builder; |
| | | 8 | | using Microsoft.AspNetCore.Http; |
| | | 9 | | using Microsoft.AspNetCore.RateLimiting; |
| | | 10 | | |
| | | 11 | | namespace Elsa.ExternalAuthentication.Endpoints.Previews; |
| | | 12 | | |
| | | 13 | | internal sealed class InitiatePreview(PreviewSignInService previews, ITenantAccessor tenantAccessor) : ElsaEndpointWitho |
| | | 14 | | { |
| | | 15 | | public override void Configure() |
| | | 16 | | { |
| | | 17 | | Post("/external-authentication/connections/{connectionId}/preview"); |
| | | 18 | | ConfigurePermissions(ExternalAuthenticationPermissions.ConnectionsPreview); |
| | | 19 | | } |
| | | 20 | | |
| | | 21 | | public override async Task HandleAsync(CancellationToken cancellationToken) |
| | | 22 | | { |
| | | 23 | | if (!Endpoints.Connections.ConnectionEndpointSupport.TryGetExpectedRevision(HttpContext, out var revision)) |
| | | 24 | | { |
| | | 25 | | HttpContext.Response.StatusCode = StatusCodes.Status428PreconditionRequired; |
| | | 26 | | return; |
| | | 27 | | } |
| | | 28 | | var result = await previews.InitiateAsync(Route<string>("connectionId")!, revision, tenantAccessor.TenantId, Use |
| | | 29 | | switch (result) |
| | | 30 | | { |
| | | 31 | | case PreviewInitiationResult.Started(var handle, var expiresAt): |
| | | 32 | | await HttpContext.Response.WriteAsJsonAsync(new PreviewInitiationResponse(BuildAuthorizePath(HttpContext |
| | | 33 | | return; |
| | | 34 | | case PreviewInitiationResult.PreconditionFailed(var currentRevision): |
| | | 35 | | HttpContext.Response.StatusCode = StatusCodes.Status412PreconditionFailed; |
| | | 36 | | await HttpContext.Response.WriteAsJsonAsync(new { error = "precondition_failed", currentRevision }, canc |
| | | 37 | | return; |
| | | 38 | | case PreviewInitiationResult.NotFound: |
| | | 39 | | HttpContext.Response.StatusCode = StatusCodes.Status404NotFound; |
| | | 40 | | return; |
| | | 41 | | default: |
| | | 42 | | HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; |
| | | 43 | | return; |
| | | 44 | | } |
| | | 45 | | } |
| | | 46 | | |
| | | 47 | | internal static string BuildAuthorizePath(PathString pathBase, PathString requestPath, string handle) |
| | | 48 | | { |
| | | 49 | | const string routeMarker = "/external-authentication/connections/"; |
| | | 50 | | var requestPathValue = requestPath.Value ?? string.Empty; |
| | | 51 | | var markerIndex = requestPathValue.LastIndexOf(routeMarker, StringComparison.OrdinalIgnoreCase); |
| | | 52 | | var routePrefix = markerIndex >= 0 ? requestPathValue[..markerIndex].TrimEnd('/') : string.Empty; |
| | | 53 | | var applicationPrefix = pathBase.Value?.TrimEnd('/') ?? string.Empty; |
| | | 54 | | return $"{applicationPrefix}{routePrefix}/external-authentication/previews/{Uri.EscapeDataString(handle)}/author |
| | | 55 | | } |
| | | 56 | | } |
| | | 57 | | |
| | 4 | 58 | | internal sealed class AuthorizePreview(PreviewSignInService previews) : ElsaEndpointWithoutRequest |
| | | 59 | | { |
| | | 60 | | public override void Configure() |
| | | 61 | | { |
| | 2 | 62 | | Get("/external-authentication/previews/{previewHandle}/authorize"); |
| | 2 | 63 | | AllowAnonymous(); |
| | 4 | 64 | | Options(x => x.RequireRateLimiting(ExternalAuthenticationRateLimitPolicyNames.ExternalInitiation)); |
| | 2 | 65 | | } |
| | | 66 | | |
| | | 67 | | public override async Task HandleAsync(CancellationToken cancellationToken) |
| | | 68 | | { |
| | 2 | 69 | | var result = await previews.AuthorizeAsync(Route<string>("previewHandle")!, cancellationToken); |
| | 2 | 70 | | if (result is PreviewAuthorizeResult.Redirect(var navigationUri)) |
| | | 71 | | { |
| | 1 | 72 | | await Send.RedirectAsync(navigationUri.ToString(), allowRemoteRedirects: true); |
| | 1 | 73 | | return; |
| | | 74 | | } |
| | 1 | 75 | | await Send.StatusCodeAsync(StatusCodes.Status410Gone, cancellationToken); |
| | 2 | 76 | | } |
| | | 77 | | } |
| | | 78 | | |
| | | 79 | | internal sealed class CompletePreview(PreviewSignInService previews) : ElsaEndpointWithoutRequest |
| | | 80 | | { |
| | | 81 | | public override void Configure() |
| | | 82 | | { |
| | | 83 | | Get("/external-authentication/previews/callback/{connectionId}"); |
| | | 84 | | AllowAnonymous(); |
| | | 85 | | Options(x => x.RequireRateLimiting(ExternalAuthenticationRateLimitPolicyNames.ProviderCallback)); |
| | | 86 | | } |
| | | 87 | | |
| | | 88 | | public override async Task HandleAsync(CancellationToken cancellationToken) |
| | | 89 | | { |
| | | 90 | | var parameters = HttpContext.Request.Query.ToDictionary(x => x.Key, x => (IReadOnlyCollection<string>)x.Value.Wh |
| | | 91 | | var result = await previews.CompleteAsync(Route<string>("connectionId")!, HttpContext.Request.Query["state"].ToS |
| | | 92 | | HttpContext.Response.StatusCode = result is PreviewCallbackResult.Completed ? StatusCodes.Status200OK : StatusCo |
| | | 93 | | await HttpContext.Response.WriteAsJsonAsync(new { completed = result is PreviewCallbackResult.Completed }, cance |
| | | 94 | | } |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | internal sealed class GetPreviewResult(PreviewSignInService previews, ITenantAccessor tenantAccessor) : ElsaEndpointWith |
| | | 98 | | { |
| | | 99 | | public override void Configure() |
| | | 100 | | { |
| | | 101 | | Get("/external-authentication/previews/{previewHandle}"); |
| | | 102 | | ConfigurePermissions(ExternalAuthenticationPermissions.ConnectionsPreview); |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | public override async Task HandleAsync(CancellationToken cancellationToken) |
| | | 106 | | { |
| | | 107 | | var result = await previews.TakeResultAsync(Route<string>("previewHandle")!, tenantAccessor.TenantId, User, canc |
| | | 108 | | if (result is TakeResult<PreviewResult>.Taken { Value: var preview }) |
| | | 109 | | { |
| | | 110 | | await HttpContext.Response.WriteAsJsonAsync(PreviewResultDocument.From(preview), cancellationToken); |
| | | 111 | | return; |
| | | 112 | | } |
| | | 113 | | var statusCode = result is TakeResult<PreviewResult>.Expired or TakeResult<PreviewResult>.AlreadyConsumed ? Stat |
| | | 114 | | await Send.StatusCodeAsync(statusCode, cancellationToken); |
| | | 115 | | } |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | internal sealed record PreviewInitiationResponse(string NavigationUrl, DateTimeOffset ExpiresAt); |
| | | 119 | | internal sealed record PreviewResultDocument(string ConnectionId, string MaterialRevision, string Issuer, string MaskedS |
| | | 120 | | { |
| | | 121 | | public static PreviewResultDocument From(PreviewResult value) => new(value.ConnectionId, value.MaterialRevision, val |
| | | 122 | | } |