diff --git a/src/modules/Elsa.Studio.Login/Extensions/StringExtensions.cs b/src/modules/Elsa.Studio.Login/Extensions/StringExtensions.cs new file mode 100644 index 000000000..d064c22fc --- /dev/null +++ b/src/modules/Elsa.Studio.Login/Extensions/StringExtensions.cs @@ -0,0 +1,29 @@ +namespace Elsa.Studio.Login.Extensions; + +/// +/// Provides a set of extension methods for . +/// +internal static class StringExtensions +{ + /// + /// Ensures the string starts with the specified character when non-empty. + /// + public static string EnsureStartsWith(this string? value, string prefix) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + return value.StartsWith(prefix) ? value : prefix + value; + } + + /// + /// Ensures the string ends with the specified string when non-empty. + /// + public static string EnsureEndsWith(this string? value, string suffix) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + return value.EndsWith(suffix) ? value : value + suffix; + } +} diff --git a/src/modules/Elsa.Studio.Login/Models/OpenIdConnectConfiguration.cs b/src/modules/Elsa.Studio.Login/Models/OpenIdConnectConfiguration.cs index 509a0ad4f..741c14056 100644 --- a/src/modules/Elsa.Studio.Login/Models/OpenIdConnectConfiguration.cs +++ b/src/modules/Elsa.Studio.Login/Models/OpenIdConnectConfiguration.cs @@ -20,6 +20,14 @@ public class OpenIdConnectConfiguration /// public required string EndSessionEndpoint { get; set; } + /// + /// A prefix to insert before /signin-oidc when constructing the redirect_uri for the authorization request. + /// Useful for sub-path deployments behind a reverse proxy, e.g. setting this to /workflow produces + /// https://myapp.com/workflow/signin-oidc. The value must start with /. When not set the redirect_uri + /// defaults to {origin}/signin-oidc. + /// + public string? RedirectUriPrefix { get; set; } + /// /// The client_id as which this application is registered with the authorization server /// diff --git a/src/modules/Elsa.Studio.Login/Services/OpenIdConnectAuthorizationService.cs b/src/modules/Elsa.Studio.Login/Services/OpenIdConnectAuthorizationService.cs index 1c09e3f84..579edaef4 100644 --- a/src/modules/Elsa.Studio.Login/Services/OpenIdConnectAuthorizationService.cs +++ b/src/modules/Elsa.Studio.Login/Services/OpenIdConnectAuthorizationService.cs @@ -1,4 +1,5 @@ -using Elsa.Studio.Login.Contracts; +using Elsa.Studio.Login.Contracts; +using Elsa.Studio.Login.Extensions; using Elsa.Studio.Login.Models; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.WebUtilities; @@ -21,7 +22,7 @@ public class OpenIdConnectAuthorizationService(IJwtAccessor jwtAccessor, IOption public async Task RedirectToAuthorizationServer() { var config = configuration.Value; - var redirectUri = new Uri(navigationManager.Uri).GetLeftPart(UriPartial.Authority) + "/signin-oidc"; + var redirectUri = new Uri(navigationManager.Uri).GetLeftPart(UriPartial.Authority) + config.RedirectUriPrefix.EnsureStartsWith("/") + "/signin-oidc"; string url = config.AuthEndpoint + $"?client_id={WebUtility.UrlEncode(config.ClientId)}&redirect_uri={WebUtility.UrlEncode(redirectUri)}&response_type=code&scope={WebUtility.UrlEncode(String.Join(' ', config.Scopes))}"; if (config.UsePkce) { @@ -40,7 +41,7 @@ public async Task RedirectToAuthorizationServer() public async Task ReceiveAuthorizationCode(string code, string? state, CancellationToken cancellationToken) { var config = configuration.Value; - var redirectUri = new Uri(navigationManager.Uri).GetLeftPart(UriPartial.Authority) + "/signin-oidc"; + var redirectUri = new Uri(navigationManager.Uri).GetLeftPart(UriPartial.Authority) + config.RedirectUriPrefix.EnsureStartsWith("/") + "/signin-oidc"; var formValues = new List> { @@ -50,7 +51,7 @@ public async Task ReceiveAuthorizationCode(string code, string? state, Cancellat new("redirect_uri", redirectUri) }; - if(!string.IsNullOrWhiteSpace(config.ClientSecret)) + if (!string.IsNullOrWhiteSpace(config.ClientSecret)) { formValues.Add(new KeyValuePair("client_secret", config.ClientSecret)); } @@ -132,7 +133,7 @@ private static async Task ReadErrorSummaryAsync(HttpContent content, Can if (summary.Length > MaxLoggedErrorLength) summary = summary[..MaxLoggedErrorLength]; - return truncated ? $"{summary}…" : summary; + return truncated ? $"{summary}…" : summary; } private static async Task<(string Content, bool Truncated)> ReadContentSnippetAsync(HttpContent content, CancellationToken cancellationToken) @@ -178,4 +179,5 @@ private static async Task ReadErrorSummaryAsync(HttpContent content, Can return null; } + }