From bd757107fecd9cd2923628274a5847be4bcde1ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:27:20 +0000 Subject: [PATCH 1/3] Initial plan From 9765274def9d42ee8a03817fc707d750e165fcbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:36:53 +0000 Subject: [PATCH 2/3] feat: add registration bootstrap flow --- Documentation/aspire/index.md | 17 +++- Documentation/configuration/invites.md | 21 ++++- Source/Aspire/AuthProxyExtensions.cs | 36 ++++++++ .../when_request_is_registration.cs | 41 +++++++++ .../and_registration_url_is_configured.cs | 54 +++++++++++ .../and_single_provider_is_configured.cs | 58 ++++++++++++ ...path_is_requested_with_lobby_configured.cs | 53 +++++++++++ .../SelectProviderMiddleware.cs | 1 + Source/AuthProxy/Configuration/Invite.cs | 2 +- Source/AuthProxy/Configuration/Service.cs | 7 ++ Source/AuthProxy/Cookies.cs | 5 ++ Source/AuthProxy/HttpContextExtensions.cs | 14 +++ Source/AuthProxy/IngressExtensions.cs | 2 + .../Registrations/RegistrationMiddleware.cs | 89 +++++++++++++++++++ Source/AuthProxy/TenancyMiddleware.cs | 10 ++- Source/AuthProxy/TenantSelectionMiddleware.cs | 4 +- Source/AuthProxy/WellKnownPaths.cs | 5 ++ 17 files changed, 409 insertions(+), 10 deletions(-) create mode 100644 Source/AuthProxy.Specs/Authentication/for_SelectProviderMiddleware/when_request_is_registration.cs create mode 100644 Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_authenticated_user_has_pending_registration/and_registration_url_is_configured.cs create mode 100644 Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_registration_path_is_requested/and_single_provider_is_configured.cs create mode 100644 Source/AuthProxy.Specs/for_TenancyMiddleware/when_tenant_resolution_fails/and_registration_path_is_requested_with_lobby_configured.cs create mode 100644 Source/AuthProxy/Registrations/RegistrationMiddleware.cs diff --git a/Documentation/aspire/index.md b/Documentation/aspire/index.md index 80c88bc..a031e08 100644 --- a/Documentation/aspire/index.md +++ b/Documentation/aspire/index.md @@ -194,7 +194,7 @@ custom selection page and the full flow. --- -## Invites and lobby +## Invites, registration and lobby ### Core invite configuration @@ -256,5 +256,18 @@ authproxy `WithLobbyFrontend` and `WithLobbyBackend` both accept an optional `endpointName` parameter (defaults to `"http"`). -See [Invites & Lobby](../configuration/invites.md) for the full invite flow walkthrough. +### Registration +To send users through the AuthProxy registration bootstrap flow, configure a lobby registration URL: + +```csharp +authproxy.WithLobbyRegistration(lobbyResource, "/register"); + +// or use a raw URL +authproxy.WithLobbyRegistration("https://lobby.example.com/register"); +``` + +This sets `Cratis:AuthProxy:Invite:Lobby:Registration:BaseUrl`. Users who visit `/register` +authenticate through AuthProxy and are then redirected to that registration URL. + +See [Invites, Registration & Lobby](../configuration/invites.md) for the full onboarding flow walkthrough. diff --git a/Documentation/configuration/invites.md b/Documentation/configuration/invites.md index 4f1cfbe..2df3d7f 100644 --- a/Documentation/configuration/invites.md +++ b/Documentation/configuration/invites.md @@ -1,8 +1,8 @@ -# Invites & Lobby +# Invites, Registration & Lobby AuthProxy includes a two-phase invite flow that lets you onboard new users via signed JWT invite -tokens, and an optional **lobby** service to which users without a resolved tenant are -redirected while they complete the onboarding process. +tokens, a registration bootstrap flow, and an optional **lobby** service to which users without a +resolved tenant are redirected while they complete onboarding. --- @@ -32,6 +32,16 @@ redirected while they complete the onboarding process. 4. If the exchange succeeds and a **lobby** service is configured, the user is redirected to the lobby's frontend so they can enter the application with their newly assigned tenant. +### Registration flow + +1. A user visits `https://your-authproxy/register`. +2. AuthProxy stores a short-lived registration cookie and starts authentication: + - If **only one** identity provider is configured, AuthProxy challenges it directly. + - If **multiple** identity providers are configured, AuthProxy redirects to the standard + provider-selection page. +3. After a successful login AuthProxy deletes the registration cookie and, when + `Invite.Lobby.Registration.BaseUrl` is configured, redirects the user to that URL. + ### Lobby – no-tenant redirect Tenancy is resolved **before** the invite system. @@ -68,7 +78,8 @@ All invite and lobby settings live under `Cratis:AuthProxy:Invite`: ], "Lobby": { "Frontend": { "BaseUrl": "http://lobby-service:3000/" }, - "Backend": { "BaseUrl": "http://lobby-service:8080/" } + "Backend": { "BaseUrl": "http://lobby-service:8080/" }, + "Registration": { "BaseUrl": "http://lobby-service:3000/register" } } } } @@ -119,6 +130,7 @@ and can be used if the lobby needs an API. |----------|------|-------------| | `Frontend.BaseUrl` | `string` | URL to which users without a tenant (or after invite exchange) are redirected. | | `Backend.BaseUrl` | `string` | Optional backend API URL for the lobby service. | +| `Registration.BaseUrl` | `string` | Optional URL to which authenticated users are redirected after `GET /register` completes. | --- @@ -143,6 +155,7 @@ Recommended claims: | Path | Description | |------|-------------| | `/invite/` | Phase 1 – validates the token and starts the OIDC flow. | +| `/register` | Starts the registration bootstrap flow and redirects to `Lobby.Registration.BaseUrl` after login. | --- diff --git a/Source/Aspire/AuthProxyExtensions.cs b/Source/Aspire/AuthProxyExtensions.cs index fbfa903..e89938a 100644 --- a/Source/Aspire/AuthProxyExtensions.cs +++ b/Source/Aspire/AuthProxyExtensions.cs @@ -636,6 +636,42 @@ public static IResourceBuilder WithLobbyBackend( ReferenceExpression.Create($"{endpoint}/")); } + /// + /// Configures the AuthProxy lobby registration URL directly. + /// This is the URL users are redirected to after completing the AuthProxy registration bootstrap flow. + /// + /// The resource type (must support environment variables). + /// The resource builder. + /// Absolute URL for the lobby registration flow. + /// The same for chaining. + public static IResourceBuilder WithLobbyRegistration( + this IResourceBuilder builder, + string registrationUrl) + where T : IResourceWithEnvironment => + builder.WithEnvironment($"{ConfigPrefix}__Invite__Lobby__Registration__BaseUrl", registrationUrl); + + /// + /// Configures the AuthProxy lobby registration URL from the specified Aspire service resource. + /// + /// The resource type (must support environment variables). + /// The resource builder. + /// The Aspire resource that exposes the lobby registration endpoint. + /// The route on the lobby service that starts registration, e.g. "/register". + /// The endpoint name to use. Defaults to "http". + /// The same for chaining. + public static IResourceBuilder WithLobbyRegistration( + this IResourceBuilder builder, + IResourceBuilder serviceResource, + string route, + string endpointName = "http") + where T : IResourceWithEnvironment + { + var endpoint = serviceResource.GetEndpoint(endpointName); + return builder.WithEnvironment(context => + context.EnvironmentVariables[$"{ConfigPrefix}__Invite__Lobby__Registration__BaseUrl"] = + ReferenceExpression.Create($"{endpoint}{route}")); + } + static IResourceBuilder AddTenantResolution(IResourceBuilder builder, string strategy) where T : IResourceWithEnvironment { diff --git a/Source/AuthProxy.Specs/Authentication/for_SelectProviderMiddleware/when_request_is_registration.cs b/Source/AuthProxy.Specs/Authentication/for_SelectProviderMiddleware/when_request_is_registration.cs new file mode 100644 index 0000000..05f714e --- /dev/null +++ b/Source/AuthProxy.Specs/Authentication/for_SelectProviderMiddleware/when_request_is_registration.cs @@ -0,0 +1,41 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.AuthProxy.Authentication.for_SelectProviderMiddleware; + +public class when_request_is_registration : Specification +{ + SelectProviderMiddleware _middleware; + DefaultHttpContext _context; + bool _nextCalled; + + void Establish() + { + var proxyConfig = Substitute.For>(); + proxyConfig.CurrentValue.Returns(new C.AuthProxy()); + + var authConfig = Substitute.For>(); + authConfig.CurrentValue.Returns(new C.Authentication + { + OidcProviders = [new C.OidcProvider { Name = "provider1", Authority = "https://auth.example.com", ClientId = "id" }] + }); + + _middleware = new SelectProviderMiddleware( + _ => + { + _nextCalled = true; + return Task.CompletedTask; + }, + proxyConfig, + authConfig, + Substitute.For(), + Substitute.For()); + + _context = new DefaultHttpContext(); + _context.Request.Path = WellKnownPaths.Registration; + } + + async Task Because() => await _middleware.InvokeAsync(_context); + + [Fact] void should_call_next() => _nextCalled.ShouldBeTrue(); +} diff --git a/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_authenticated_user_has_pending_registration/and_registration_url_is_configured.cs b/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_authenticated_user_has_pending_registration/and_registration_url_is_configured.cs new file mode 100644 index 0000000..6307f75 --- /dev/null +++ b/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_authenticated_user_has_pending_registration/and_registration_url_is_configured.cs @@ -0,0 +1,54 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.AuthProxy.Invites; + +namespace Cratis.AuthProxy.Registrations.for_RegistrationMiddleware.when_authenticated_user_has_pending_registration; + +public class and_registration_url_is_configured : Specification +{ + const string RegistrationUrl = "http://lobby.example.com/register"; + + RegistrationMiddleware _middleware; + DefaultHttpContext _context; + bool _nextCalled; + + void Establish() + { + var proxyConfig = Substitute.For>(); + proxyConfig.CurrentValue.Returns(new C.AuthProxy + { + Invite = new C.Invite + { + Lobby = new C.Service + { + Registration = new C.ServiceEndpoint { BaseUrl = RegistrationUrl } + } + } + }); + + var authConfig = Substitute.For>(); + authConfig.CurrentValue.Returns(new C.Authentication()); + + _middleware = new RegistrationMiddleware( + _ => + { + _nextCalled = true; + return Task.CompletedTask; + }, + proxyConfig, + authConfig, + Substitute.For()); + + _context = new DefaultHttpContext(); + _context.Request.Path = "/"; + _context.Request.Headers.Cookie = $"{Cookies.Registration}=pending"; + _context.User = new ClaimsPrincipal(new ClaimsIdentity([new Claim("sub", "user-123")], "aad")); + } + + async Task Because() => await _middleware.InvokeAsync(_context); + + [Fact] void should_call_next() => _nextCalled.ShouldBeTrue(); + [Fact] void should_set_lobby_redirect_url_in_context_items() => _context.Items[InviteMiddleware.LobbyRedirectUrlItemKey].ShouldEqual(RegistrationUrl); + [Fact] void should_delete_registration_cookie() => _context.Response.Headers.SetCookie.ToString().ShouldContain(Cookies.Registration); +} diff --git a/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_registration_path_is_requested/and_single_provider_is_configured.cs b/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_registration_path_is_requested/and_single_provider_is_configured.cs new file mode 100644 index 0000000..9b77478 --- /dev/null +++ b/Source/AuthProxy.Specs/Registrations/for_RegistrationMiddleware/when_registration_path_is_requested/and_single_provider_is_configured.cs @@ -0,0 +1,58 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.AspNetCore.Authentication; + +namespace Cratis.AuthProxy.Registrations.for_RegistrationMiddleware.when_registration_path_is_requested; + +public class and_single_provider_is_configured : Specification +{ + RegistrationMiddleware _middleware; + DefaultHttpContext _context; + string _challengedScheme = string.Empty; + + void Establish() + { + var proxyConfig = Substitute.For>(); + proxyConfig.CurrentValue.Returns(new C.AuthProxy + { + Invite = new C.Invite + { + Lobby = new C.Service + { + Registration = new C.ServiceEndpoint { BaseUrl = "http://lobby.example.com/register" } + } + } + }); + + var authConfig = Substitute.For>(); + authConfig.CurrentValue.Returns(new C.Authentication + { + OidcProviders = [new C.OidcProvider { Name = "provider1", Authority = "https://auth.example.com", ClientId = "id" }] + }); + + _middleware = new RegistrationMiddleware( + _ => Task.CompletedTask, + proxyConfig, + authConfig, + Substitute.For()); + + _context = new DefaultHttpContext(); + _context.Request.Path = WellKnownPaths.Registration; + _context.Response.Body = new System.IO.MemoryStream(); + + var authService = Substitute.For(); + authService + .ChallengeAsync(Arg.Any(), Arg.Do(s => _challengedScheme = s), Arg.Any()) + .Returns(Task.CompletedTask); + + var serviceProvider = Substitute.For(); + serviceProvider.GetService(typeof(IAuthenticationService)).Returns(authService); + _context.RequestServices = serviceProvider; + } + + async Task Because() => await _middleware.InvokeAsync(_context); + + [Fact] void should_challenge_with_provider_scheme() => _challengedScheme.ShouldEqual(OidcProviderScheme.FromName("provider1")); + [Fact] void should_set_registration_cookie() => _context.Response.Headers.SetCookie.ToString().ShouldContain(Cookies.Registration); +} diff --git a/Source/AuthProxy.Specs/for_TenancyMiddleware/when_tenant_resolution_fails/and_registration_path_is_requested_with_lobby_configured.cs b/Source/AuthProxy.Specs/for_TenancyMiddleware/when_tenant_resolution_fails/and_registration_path_is_requested_with_lobby_configured.cs new file mode 100644 index 0000000..1b1f7aa --- /dev/null +++ b/Source/AuthProxy.Specs/for_TenancyMiddleware/when_tenant_resolution_fails/and_registration_path_is_requested_with_lobby_configured.cs @@ -0,0 +1,53 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Cratis.AuthProxy.for_TenancyMiddleware.when_tenant_resolution_fails; + +public class and_registration_path_is_requested_with_lobby_configured : Specification +{ + const string LobbyUrl = "http://lobby-service/"; + + TenancyMiddleware _middleware; + DefaultHttpContext _context; + bool _nextCalled; + + void Establish() + { + var config = new C.AuthProxy + { + Invite = new C.Invite + { + RedirectToLobbyWhenTenantUnresolved = true, + Lobby = new C.Service + { + Frontend = new C.ServiceEndpoint { BaseUrl = LobbyUrl } + } + } + }; + var optionsMonitor = Substitute.For>(); + optionsMonitor.CurrentValue.Returns(config); + + var tenantResolver = Substitute.For(); + tenantResolver.TryResolve(Arg.Any(), out Arg.Any()).Returns(false); + + _middleware = new TenancyMiddleware( + _ => + { + _nextCalled = true; + return Task.CompletedTask; + }, + optionsMonitor, + tenantResolver, + Substitute.For(), + Substitute.For(), + Substitute.For>()); + + _context = new DefaultHttpContext(); + _context.Request.Path = WellKnownPaths.Registration; + } + + async Task Because() => await _middleware.InvokeAsync(_context); + + [Fact] void should_call_next() => _nextCalled.ShouldBeTrue(); + [Fact] void should_not_redirect_to_lobby() => _context.Response.Headers.Location.ToString().ShouldEqual(string.Empty); +} diff --git a/Source/AuthProxy/Authentication/SelectProviderMiddleware.cs b/Source/AuthProxy/Authentication/SelectProviderMiddleware.cs index 2a0175a..77ace5f 100644 --- a/Source/AuthProxy/Authentication/SelectProviderMiddleware.cs +++ b/Source/AuthProxy/Authentication/SelectProviderMiddleware.cs @@ -41,6 +41,7 @@ public async Task InvokeAsync(HttpContext context) { if (context.User.Identity?.IsAuthenticated == true || context.IsInvitation() + || context.IsRegistration() || context.IsAuthenticationUI() || context.HasPendingInvitation()) { diff --git a/Source/AuthProxy/Configuration/Invite.cs b/Source/AuthProxy/Configuration/Invite.cs index 1a1b9e1..8769863 100644 --- a/Source/AuthProxy/Configuration/Invite.cs +++ b/Source/AuthProxy/Configuration/Invite.cs @@ -79,7 +79,7 @@ public class Invite /// /// Gets or sets the lobby service configuration. /// When set, requests from users without a resolved tenant are forwarded to this service's frontend, - /// and users are redirected here after a successful invite exchange. + /// invite exchanges return here, and registrations can redirect to its configured registration endpoint. /// public Service? Lobby { get; set; } } diff --git a/Source/AuthProxy/Configuration/Service.cs b/Source/AuthProxy/Configuration/Service.cs index 479f0ff..86d0ba8 100644 --- a/Source/AuthProxy/Configuration/Service.cs +++ b/Source/AuthProxy/Configuration/Service.cs @@ -18,6 +18,13 @@ public class Service /// public ServiceEndpoint? Frontend { get; set; } + /// + /// Gets or sets the registration endpoint for this service. + /// This is currently used by the lobby configuration to identify where new users should be sent + /// after the AuthProxy registration flow completes. + /// + public ServiceEndpoint? Registration { get; set; } + /// /// Gets or sets whether to call the /.cratis/me identity endpoint on this service /// to enrich the identity details cookie. Defaults to when a Backend is configured. diff --git a/Source/AuthProxy/Cookies.cs b/Source/AuthProxy/Cookies.cs index 7bb0d72..22b808c 100644 --- a/Source/AuthProxy/Cookies.cs +++ b/Source/AuthProxy/Cookies.cs @@ -18,6 +18,11 @@ public static class Cookies /// public const string InviteToken = ".cratis-invite"; + /// + /// Short-lived HTTP-only cookie used to carry an in-flight registration across the OIDC redirect. + /// + public const string Registration = ".cratis-registration"; + /// /// Short-lived cookie injected by the proxy when serving the invitation provider-selection page. /// Contains a JSON array of OidcProviderInfo objects so the page can render diff --git a/Source/AuthProxy/HttpContextExtensions.cs b/Source/AuthProxy/HttpContextExtensions.cs index 5a4a25c..77eac06 100644 --- a/Source/AuthProxy/HttpContextExtensions.cs +++ b/Source/AuthProxy/HttpContextExtensions.cs @@ -24,6 +24,13 @@ public static class HttpContextExtensions /// if a pending invitation cookie exists; otherwise . public static bool HasPendingInvitation(this HttpContext context) => context.Request.Cookies.ContainsKey(Cookies.InviteToken); + /// + /// Determines whether the request has a pending registration cookie. + /// + /// The to evaluate. + /// if a pending registration cookie exists; otherwise . + public static bool HasPendingRegistration(this HttpContext context) => context.Request.Cookies.ContainsKey(Cookies.Registration); + /// /// Determines whether the request targets an invitation URL. /// @@ -31,6 +38,13 @@ public static class HttpContextExtensions /// if the request is an invitation URL; otherwise . public static bool IsInvitation(this HttpContext context) => context.Request.Path.StartsWithSegments(WellKnownPaths.InvitePathPrefix); + /// + /// Determines whether the request targets the registration URL. + /// + /// The to evaluate. + /// if the request is a registration URL; otherwise . + public static bool IsRegistration(this HttpContext context) => context.Request.Path.StartsWithSegments(WellKnownPaths.Registration); + /// /// Determines whether the request targets one of the login endpoints. /// diff --git a/Source/AuthProxy/IngressExtensions.cs b/Source/AuthProxy/IngressExtensions.cs index c95aca8..d867484 100644 --- a/Source/AuthProxy/IngressExtensions.cs +++ b/Source/AuthProxy/IngressExtensions.cs @@ -5,6 +5,7 @@ using Cratis.AuthProxy.ErrorPages; using Cratis.AuthProxy.Identity; using Cratis.AuthProxy.Invites; +using Cratis.AuthProxy.Registrations; using Cratis.AuthProxy.ReverseProxy; using Cratis.AuthProxy.Tenancy; using Microsoft.AspNetCore.Authentication; @@ -73,6 +74,7 @@ public static WebApplication UseIngress(this WebApplication app) app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); + app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); diff --git a/Source/AuthProxy/Registrations/RegistrationMiddleware.cs b/Source/AuthProxy/Registrations/RegistrationMiddleware.cs new file mode 100644 index 0000000..39745cc --- /dev/null +++ b/Source/AuthProxy/Registrations/RegistrationMiddleware.cs @@ -0,0 +1,89 @@ +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.AuthProxy.Authentication; +using Cratis.AuthProxy.Invites; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Options; +using C = Cratis.AuthProxy.Configuration; + +namespace Cratis.AuthProxy.Registrations; + +/// +/// Middleware that implements the AuthProxy registration bootstrap flow. +/// Visiting /register stores a short-lived cookie and either redirects to the generic +/// provider-selection page or immediately challenges the single configured provider. +/// After authentication completes, the middleware redirects the user to the configured +/// lobby registration endpoint. +/// +/// The next middleware in the pipeline. +/// The auth proxy configuration monitor. +/// The authentication configuration monitor. +/// The tenant resolver used to capture tenant metadata in authentication state. +public class RegistrationMiddleware( + RequestDelegate next, + IOptionsMonitor proxyConfig, + IOptionsMonitor authConfig, + ITenantResolver tenantResolver) +{ + /// + public async Task InvokeAsync(HttpContext context) + { + if (context.User.Identity?.IsAuthenticated == true && context.HasPendingRegistration()) + { + context.Response.Cookies.Delete(Cookies.Registration); + + var registrationUrl = proxyConfig.CurrentValue.Invite?.Lobby?.Registration?.BaseUrl; + if (!string.IsNullOrWhiteSpace(registrationUrl)) + { + context.Items[InviteMiddleware.LobbyRedirectUrlItemKey] = registrationUrl; + } + + await next(context); + return; + } + + if (!context.IsRegistration()) + { + await next(context); + return; + } + + var registrationUrlConfigured = !string.IsNullOrWhiteSpace(proxyConfig.CurrentValue.Invite?.Lobby?.Registration?.BaseUrl); + if (!registrationUrlConfigured) + { + await next(context); + return; + } + + context.Response.Cookies.Append(Cookies.Registration, "pending", new CookieOptions + { + HttpOnly = true, + SameSite = SameSiteMode.Lax, + Secure = context.Request.IsHttps, + MaxAge = TimeSpan.FromMinutes(15), + }); + + var providers = GetAllProviders(authConfig.CurrentValue).ToList(); + if (providers.Count > 1) + { + context.Response.Redirect(WellKnownPaths.LoginPage); + return; + } + + if (providers.Count == 1) + { + var scheme = OidcProviderScheme.FromName(providers[0].Name); + var returnUrl = context.GetPathAndQuery(); + var properties = TenantAuthenticationState.CreateChallengeProperties(context, tenantResolver, returnUrl); + await context.ChallengeAsync(scheme, properties); + return; + } + + await next(context); + } + + static IEnumerable GetAllProviders(C.Authentication config) => + config.OidcProviders.Select(OidcProviderScheme.ToProviderInfo) + .Concat(config.OAuthProviders.Select(OidcProviderScheme.ToProviderInfo)); +} diff --git a/Source/AuthProxy/TenancyMiddleware.cs b/Source/AuthProxy/TenancyMiddleware.cs index 9b46d49..1236b27 100644 --- a/Source/AuthProxy/TenancyMiddleware.cs +++ b/Source/AuthProxy/TenancyMiddleware.cs @@ -61,13 +61,17 @@ public async Task InvokeAsync(HttpContext context) var lobbyUrl = config.CurrentValue.Invite?.Lobby?.Frontend?.BaseUrl; var shouldRedirectToLobby = config.CurrentValue.Invite?.RedirectToLobbyWhenTenantUnresolved == true; var isInvitePath = context.IsInvitation(); + var isRegistrationPath = context.IsRegistration(); var hasPendingInviteCookie = context.HasPendingInvitation(); + var hasPendingRegistrationCookie = context.HasPendingRegistration(); var isAuthPath = context.IsAuthenticationUI(); if (shouldRedirectToLobby && !string.IsNullOrWhiteSpace(lobbyUrl) && !isInvitePath + && !isRegistrationPath && !isAuthPath - && !hasPendingInviteCookie) + && !hasPendingInviteCookie + && !hasPendingRegistrationCookie) { logger.RedirectingToLobby(context.Request.Path); context.Response.Redirect(lobbyUrl); @@ -77,7 +81,9 @@ public async Task InvokeAsync(HttpContext context) if (config.CurrentValue.TenantResolutions.Count > 0 && !isAuthPath && !isInvitePath - && !hasPendingInviteCookie) + && !isRegistrationPath + && !hasPendingInviteCookie + && !hasPendingRegistrationCookie) { logger.CouldNotResolveTenant(context.Request.Path); context.Response.StatusCode = StatusCodes.Status401Unauthorized; diff --git a/Source/AuthProxy/TenantSelectionMiddleware.cs b/Source/AuthProxy/TenantSelectionMiddleware.cs index 1a324e8..0ae8481 100644 --- a/Source/AuthProxy/TenantSelectionMiddleware.cs +++ b/Source/AuthProxy/TenantSelectionMiddleware.cs @@ -34,8 +34,10 @@ public async Task InvokeAsync(HttpContext context) { if (context.User.Identity?.IsAuthenticated != true || context.IsInvitation() + || context.IsRegistration() || context.IsAuthenticationBootstrap() - || context.HasPendingInvitation()) + || context.HasPendingInvitation() + || context.HasPendingRegistration()) { await next(context); return; diff --git a/Source/AuthProxy/WellKnownPaths.cs b/Source/AuthProxy/WellKnownPaths.cs index 30fa933..4327f1f 100644 --- a/Source/AuthProxy/WellKnownPaths.cs +++ b/Source/AuthProxy/WellKnownPaths.cs @@ -42,6 +42,11 @@ public static class WellKnownPaths /// public const string InvitePathPrefix = "/invite"; + /// + /// The well-known path that triggers the registration flow. + /// + public const string Registration = "/register"; + /// /// The request path prefix under which custom pages (error pages, login UI, etc.) are served as static files. /// All requests under this prefix are always allowed anonymously. From 80430b7b3dd8b6fdffad4fb0f3f220ac14f9810e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:15:48 +0000 Subject: [PATCH 3/3] docs: reorganize lobby onboarding documentation --- Documentation/aspire/index.md | 2 +- Documentation/configuration/error-pages.md | 2 +- Documentation/configuration/index.md | 2 +- Documentation/configuration/invites.md | 189 ------------------ Documentation/configuration/lobby/index.md | 49 +++++ .../invitation-for-creating-organization.md | 98 +++++++++ .../lobby/invitation-to-organization.md | 55 +++++ .../configuration/lobby/registration.md | 51 +++++ Documentation/configuration/lobby/toc.yml | 8 + Documentation/configuration/tenancy.md | 2 +- Documentation/configuration/toc.yml | 4 +- 11 files changed, 267 insertions(+), 195 deletions(-) delete mode 100644 Documentation/configuration/invites.md create mode 100644 Documentation/configuration/lobby/index.md create mode 100644 Documentation/configuration/lobby/invitation-for-creating-organization.md create mode 100644 Documentation/configuration/lobby/invitation-to-organization.md create mode 100644 Documentation/configuration/lobby/registration.md create mode 100644 Documentation/configuration/lobby/toc.yml diff --git a/Documentation/aspire/index.md b/Documentation/aspire/index.md index a031e08..989b5f7 100644 --- a/Documentation/aspire/index.md +++ b/Documentation/aspire/index.md @@ -270,4 +270,4 @@ authproxy.WithLobbyRegistration("https://lobby.example.com/register"); This sets `Cratis:AuthProxy:Invite:Lobby:Registration:BaseUrl`. Users who visit `/register` authenticate through AuthProxy and are then redirected to that registration URL. -See [Invites, Registration & Lobby](../configuration/invites.md) for the full onboarding flow walkthrough. +See [Lobby](../configuration/lobby/index.md) for the onboarding flow walkthroughs. diff --git a/Documentation/configuration/error-pages.md b/Documentation/configuration/error-pages.md index 913da54..274d348 100644 --- a/Documentation/configuration/error-pages.md +++ b/Documentation/configuration/error-pages.md @@ -100,7 +100,7 @@ indicating that the authenticated user's subject is already associated with an e The user should sign in with their existing account rather than completing the invitation again. If you prefer to redirect users to a custom URL instead of serving this page, configure -`Invite.SubjectAlreadyExistsUrl` (see [Invites & Lobby](invites.md#invite-properties)). +`Invite.SubjectAlreadyExistsUrl` (see [Invitation for Creating Organization](lobby/invitation-for-creating-organization.md#configuration)). --- diff --git a/Documentation/configuration/index.md b/Documentation/configuration/index.md index 16a850c..6467fee 100644 --- a/Documentation/configuration/index.md +++ b/Documentation/configuration/index.md @@ -25,5 +25,5 @@ Cratis AuthProxy is configured entirely through the `Cratis:AuthProxy` section o | [Tenancy](tenancy.md) | How the auth proxy resolves the current tenant from each request, and how to verify tenant existence. | | [Tenant Selection Page](tenant-selection.md) | How selection-based tenant resolution works and how to build/override `select-tenant.html`. | | [Services](services.md) | Routing requests to backend and frontend services. | -| [Invites & Lobby](invites.md) | Invite-based onboarding and the lobby service. | +| [Lobby](lobby/index.md) | Invite and registration flows that hand users off to the lobby experience. | | [Well-Known Pages](well-known-pages.md) | Built-in HTML pages (provider selection, errors, tenant not found) and how to override them via a mounted volume. | diff --git a/Documentation/configuration/invites.md b/Documentation/configuration/invites.md deleted file mode 100644 index 2df3d7f..0000000 --- a/Documentation/configuration/invites.md +++ /dev/null @@ -1,189 +0,0 @@ -# Invites, Registration & Lobby - -AuthProxy includes a two-phase invite flow that lets you onboard new users via signed JWT invite -tokens, a registration bootstrap flow, and an optional **lobby** service to which users without a -resolved tenant are redirected while they complete onboarding. - ---- - -## How it works - -### Phase 1 – Invite link - -1. A user receives a link in the form `https://your-authproxy/invite/`. -2. AuthProxy validates the token against the configured RSA public key. -3. If the token is **valid**, it is stored in a short-lived HTTP-only cookie. - - If **only one** identity provider is configured, the user is redirected directly to that provider's login. - - If **multiple** identity providers are configured, AuthProxy serves `invitation-select-provider.html` - with a `.cratis-providers` cookie so the user can choose which provider to log in with. -4. If the token has **expired** (valid signature but past its `exp` claim), AuthProxy serves - `invitation-expired.html` with HTTP 401. -5. If the token is **invalid** (malformed, bad signature, or unparseable), AuthProxy serves - `invitation-invalid.html` with HTTP 401. - -### Phase 2 – Post-login exchange - -1. After a successful OIDC login the user is redirected back. -2. AuthProxy detects the invite cookie, calls the configured `ExchangeUrl` with the token and the - authenticated user's subject, then deletes the cookie. -3. If the exchange endpoint returns **HTTP 409 Conflict** (subject already registered), AuthProxy - redirects to `SubjectAlreadyExistsUrl` (if configured) or serves the built-in - `invitation-subject-already-exists.html` page. -4. If the exchange succeeds and a **lobby** service is configured, the user is redirected to - the lobby's frontend so they can enter the application with their newly assigned tenant. - -### Registration flow - -1. A user visits `https://your-authproxy/register`. -2. AuthProxy stores a short-lived registration cookie and starts authentication: - - If **only one** identity provider is configured, AuthProxy challenges it directly. - - If **multiple** identity providers are configured, AuthProxy redirects to the standard - provider-selection page. -3. After a successful login AuthProxy deletes the registration cookie and, when - `Invite.Lobby.Registration.BaseUrl` is configured, redirects the user to that URL. - -### Lobby – no-tenant redirect - -Tenancy is resolved **before** the invite system. -When AuthProxy cannot resolve a tenant for a request it checks whether a lobby is configured: - -- **Lobby configured** – the user is redirected to the lobby's frontend URL, unless the request - is already an invite path (`/invite/...`) or the user already holds a pending invite cookie (so - that the Phase 2 exchange can complete). -- **No lobby** – AuthProxy returns `401 Unauthorized` when `TenantResolutions` is non-empty, - or proceeds without a tenant when no resolutions are configured. - ---- - -## Configuration - -All invite and lobby settings live under `Cratis:AuthProxy:Invite`: - -```json -{ - "Cratis": { - "AuthProxy": { - "Invite": { - "PublicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", - "Issuer": "https://studio.example.com", - "Audience": "authproxy", - "ExchangeUrl": "https://studio.example.com/internal/invites/exchange", - "TenantClaim": "tenant_id", - "SubjectAlreadyExistsUrl": "https://app.example.com/errors/account-already-exists", - "AppendInvitationIdToQueryString": true, - "InvitationIdQueryStringKey": "invitationId", - "ClaimsToForward": [ - { "FromClaimType": "organization_id", "ToClaimType": "organization" }, - { "FromClaimType": "invited_by" } - ], - "Lobby": { - "Frontend": { "BaseUrl": "http://lobby-service:3000/" }, - "Backend": { "BaseUrl": "http://lobby-service:8080/" }, - "Registration": { "BaseUrl": "http://lobby-service:3000/register" } - } - } - } - } -} -``` - -### Invite properties - -| Property | Type | Description | -|----------|------|-------------| -| `PublicKeyPem` | `string` | PEM-encoded RSA public key used to verify invite token signatures. | -| `Issuer` | `string` | Expected `iss` claim. Leave empty to skip issuer validation. | -| `Audience` | `string` | Expected `aud` claim. Leave empty to skip audience validation. | -| `ExchangeUrl` | `string` | Absolute URL of the invite-exchange endpoint, e.g. `https://studio.example.com/internal/invites/exchange`. | -| `TenantClaim` | `string` | Claim in the invite token that contains the tenant ID string for tenant-issued invite detection. | -| `SubjectAlreadyExistsUrl` | `string` | URL to redirect to when the exchange endpoint returns HTTP 409 (subject already registered). Leave empty to serve the built-in `invitation-subject-already-exists.html` page. | -| `AppendInvitationIdToQueryString` | `bool` | Appends `jti` from the invite token to the lobby redirect query string when enabled. | -| `InvitationIdQueryStringKey` | `string` | Query string key used when appending invitation ID. | -| `ClaimsToForward` | `InviteClaimForwarding[]` | Claim mappings forwarded from invite token into the principal sent to identity details providers. | -| `Lobby` | `Service` | Optional lobby service. See below. | - -### Invite claim forwarding - -When `ClaimsToForward` is configured and a pending invite token cookie exists, AuthProxy reads the configured -invite-token claims and adds them to the principal payload sent to each `/.cratis/me` identity details endpoint. - -- Existing identity-provider claims are preserved. -- Mapped invite claims are appended if present. -- If `ToClaimType` is empty, the original `FromClaimType` is used. - -This gives you a decoupled extension point for propagating invitation context into identity resolution. - -### InviteClaimForwarding properties - -| Property | Type | Description | -|----------|------|-------------| -| `FromClaimType` | `string` | Claim type to read from the invite token payload. | -| `ToClaimType` | `string` | Claim type to emit in the principal sent to identity details providers. Defaults to `FromClaimType` when empty. | - -### Lobby service - -The `Lobby` property accepts a standard [`Service`](services.md) object. -Only the `Frontend.BaseUrl` is required for the lobby redirect; a `Backend` endpoint is optional -and can be used if the lobby needs an API. - -| Property | Type | Description | -|----------|------|-------------| -| `Frontend.BaseUrl` | `string` | URL to which users without a tenant (or after invite exchange) are redirected. | -| `Backend.BaseUrl` | `string` | Optional backend API URL for the lobby service. | -| `Registration.BaseUrl` | `string` | Optional URL to which authenticated users are redirected after `GET /register` completes. | - ---- - -## Invite token format - -Invite tokens are standard JWTs signed with an RSA private key held by the issuing service -(e.g. Cratis Studio). The auth proxy only needs the matching **public key** to validate signatures. - -Recommended claims: - -| Claim | Description | -|-------|-------------| -| `iss` | Issuer – must match `Invite.Issuer` if set. | -| `aud` | Audience – must match `Invite.Audience` if set. | -| `exp` | Expiry – tokens with a past `exp` are rejected. | -| `sub` | Subject – the invited user's identifier (passed to the exchange endpoint). | - ---- - -## Well-known paths - -| Path | Description | -|------|-------------| -| `/invite/` | Phase 1 – validates the token and starts the OIDC flow. | -| `/register` | Starts the registration bootstrap flow and redirects to `Lobby.Registration.BaseUrl` after login. | - ---- - -## Invitation error pages - -AuthProxy distinguishes between two token failure modes and serves a dedicated page for each: - -| Page file | Condition | -|-----------|-----------| -| `invitation-expired.html` | The token had a valid signature but has passed its `exp` claim. | -| `invitation-invalid.html` | The token is malformed, carries an invalid signature, or cannot be parsed. | -| `invitation-select-provider.html` | The token is valid and multiple identity providers are configured. | -| `invitation-subject-already-exists.html` | The authenticated user's subject is already associated with an existing account (exchange returned HTTP 409). | - -All error pages are served with the following HTTP status codes: - -| Page | HTTP status | -|------|-------------| -| `invitation-expired.html` | 401 | -| `invitation-invalid.html` | 401 | -| `invitation-select-provider.html` | 200 | -| `invitation-subject-already-exists.html` | 409 | -These pages use full descriptive names rather than numeric error codes because they represent -application-level conditions, not generic HTTP errors. - -See [Error pages](error-pages.md) for how to override these pages with your own custom versions -and for details on the `.cratis-providers` cookie injected into the provider-selection page. - -For a full walkthrough of creating a branded custom provider-selection page, including the cookie -format, JavaScript reading pattern, asset deployment, and a complete end-to-end flow diagram, see -[Custom Invitation Provider-Selection Page](invitation-provider-selection.md). diff --git a/Documentation/configuration/lobby/index.md b/Documentation/configuration/lobby/index.md new file mode 100644 index 0000000..313fcfd --- /dev/null +++ b/Documentation/configuration/lobby/index.md @@ -0,0 +1,49 @@ +# Lobby + +AuthProxy uses the lobby to finish onboarding when a user cannot enter the application directly +after authentication. + +Use the lobby documentation based on the onboarding outcome you need: + +- [Invitation for Creating Organization](invitation-for-creating-organization.md) — invite a user + who should create a new organization after they sign in. +- [Invitation to Organization](invitation-to-organization.md) — invite a user directly into an + existing organization. +- [Registration](registration.md) — let a user start a self-serve registration flow that ends in + organization creation. If the user should join an existing organization, invite them instead. + +## Shared configuration + +All lobby-related settings live under `Cratis:AuthProxy:Invite:Lobby`: + +```json +{ + "Cratis": { + "AuthProxy": { + "Invite": { + "Lobby": { + "Frontend": { "BaseUrl": "http://lobby-service:3000/" }, + "Backend": { "BaseUrl": "http://lobby-service:8080/" }, + "Registration": { "BaseUrl": "http://lobby-service:3000/register" } + } + } + } + } +} +``` + +The `Lobby` object uses the standard [`Service`](../services.md) shape. + +| Property | Type | Description | +|----------|------|-------------| +| `Frontend.BaseUrl` | `string` | URL used for lobby redirects after onboarding or when tenant resolution fails. | +| `Backend.BaseUrl` | `string` | Optional backend API URL for the lobby service. | +| `Registration.BaseUrl` | `string` | Optional URL used after `GET /register` completes successfully. | + +## Lobby fallback + +Tenant resolution runs before invite and registration handling. + +When AuthProxy cannot resolve a tenant and a lobby frontend is configured, it redirects the user to +`Lobby.Frontend.BaseUrl` unless the request is already an onboarding bootstrap path such as +`/invite/` or `/register`. diff --git a/Documentation/configuration/lobby/invitation-for-creating-organization.md b/Documentation/configuration/lobby/invitation-for-creating-organization.md new file mode 100644 index 0000000..4d6ba72 --- /dev/null +++ b/Documentation/configuration/lobby/invitation-for-creating-organization.md @@ -0,0 +1,98 @@ +# Invitation for Creating Organization + +Use this flow when you invite a user who should create a new organization after they authenticate. +AuthProxy validates the invite, exchanges it after sign-in, and then sends the user to the lobby +frontend to finish onboarding. + +## Flow + +1. The user opens `https://your-authproxy/invite/`. +2. AuthProxy validates the signed JWT invite token. +3. If the token is valid, AuthProxy stores it in a short-lived HTTP-only cookie. + - With one configured identity provider, AuthProxy challenges that provider immediately. + - With multiple providers, AuthProxy serves `invitation-select-provider.html` so the user can + choose how to sign in. +4. After a successful login, AuthProxy calls `Invite.ExchangeUrl` with the invite token and the + authenticated user's subject. +5. If the exchange succeeds, AuthProxy redirects the user to `Invite.Lobby.Frontend.BaseUrl`. + +This flow is the right fit when the invited user is not entering an already-resolved tenant. + +## Configuration + +```json +{ + "Cratis": { + "AuthProxy": { + "Invite": { + "PublicKeyPem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", + "Issuer": "https://studio.example.com", + "Audience": "authproxy", + "ExchangeUrl": "https://studio.example.com/internal/invites/exchange", + "SubjectAlreadyExistsUrl": "https://app.example.com/errors/account-already-exists", + "AppendInvitationIdToQueryString": true, + "InvitationIdQueryStringKey": "invitationId", + "ClaimsToForward": [ + { "FromClaimType": "organization_id", "ToClaimType": "organization" }, + { "FromClaimType": "invited_by" } + ], + "Lobby": { + "Frontend": { "BaseUrl": "http://lobby-service:3000/" }, + "Backend": { "BaseUrl": "http://lobby-service:8080/" } + } + } + } + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `PublicKeyPem` | `string` | PEM-encoded RSA public key used to verify invite token signatures. | +| `Issuer` | `string` | Expected `iss` claim. Leave empty to skip issuer validation. | +| `Audience` | `string` | Expected `aud` claim. Leave empty to skip audience validation. | +| `ExchangeUrl` | `string` | Absolute URL of the invite exchange endpoint. | +| `SubjectAlreadyExistsUrl` | `string` | Redirect target when the exchange endpoint returns HTTP 409. Leave empty to serve `invitation-subject-already-exists.html`. | +| `AppendInvitationIdToQueryString` | `bool` | Appends `jti` from the invite token to the lobby redirect URL when enabled. | +| `InvitationIdQueryStringKey` | `string` | Query-string key used when appending the invitation ID. | +| `ClaimsToForward` | `InviteClaimForwarding[]` | Claim mappings forwarded from the invite token into the identity details request. | +| `Lobby.Frontend.BaseUrl` | `string` | Lobby URL used after a successful invite exchange. | + +## Invite claim forwarding + +When `ClaimsToForward` is configured and a pending invite cookie exists, AuthProxy reads the +configured invite-token claims and adds them to the principal payload sent to each `/.cratis/me` +identity details endpoint. + +- Existing identity-provider claims are preserved. +- Mapped invite claims are appended if present. +- If `ToClaimType` is empty, AuthProxy uses `FromClaimType`. + +## Invite token format + +Invite tokens are JWTs signed with an RSA private key held by the issuing service. AuthProxy only +needs the matching public key to validate the signature. + +Recommended claims: + +| Claim | Description | +|-------|-------------| +| `iss` | Issuer. Must match `Invite.Issuer` when configured. | +| `aud` | Audience. Must match `Invite.Audience` when configured. | +| `exp` | Expiry time. Expired tokens are rejected. | +| `sub` | Subject for the invited user. AuthProxy includes it in the exchange call. | + +## Error handling + +AuthProxy serves dedicated pages for each invitation error: + +| Page file | Condition | HTTP status | +|-----------|-----------|-------------| +| `invitation-expired.html` | The token signature is valid, but the `exp` claim is in the past. | 401 | +| `invitation-invalid.html` | The token is malformed or has an invalid signature. | 401 | +| `invitation-select-provider.html` | The token is valid and multiple identity providers are configured. | 200 | +| `invitation-subject-already-exists.html` | The authenticated subject is already associated with an existing account. | 409 | + +See [Error pages](../error-pages.md) for customization details and +[Custom Invitation Provider-Selection Page](../invitation-provider-selection.md) for a full branded +provider-selection walkthrough. diff --git a/Documentation/configuration/lobby/invitation-to-organization.md b/Documentation/configuration/lobby/invitation-to-organization.md new file mode 100644 index 0000000..3c547d9 --- /dev/null +++ b/Documentation/configuration/lobby/invitation-to-organization.md @@ -0,0 +1,55 @@ +# Invitation to Organization + +Use this flow when you invite a user into an organization that already exists. AuthProxy still uses +the standard `/invite/` bootstrap, but the invite token carries tenant information so the +user can continue directly into the application instead of being sent to the lobby. + +## Flow + +1. The user opens `https://your-authproxy/invite/`. +2. AuthProxy validates the token and starts authentication in the same way as any other invite. +3. After login, AuthProxy exchanges the invite at `Invite.ExchangeUrl`. +4. AuthProxy compares the configured `Invite.TenantClaim` from the token with the resolved tenant + for the request. +5. If the tenant IDs match, AuthProxy skips the lobby redirect and continues to the target service. + +If the tenant IDs do not match, or AuthProxy cannot resolve a tenant for the request, the invite is +treated like lobby onboarding and falls back to the configured lobby behavior. + +## Configuration + +```json +{ + "Cratis": { + "AuthProxy": { + "Invite": { + "ExchangeUrl": "https://studio.example.com/internal/invites/exchange", + "TenantClaim": "tenant_id", + "Lobby": { + "Frontend": { "BaseUrl": "http://lobby-service:3000/" } + } + } + } + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `ExchangeUrl` | `string` | Absolute URL of the invite exchange endpoint. | +| `TenantClaim` | `string` | Claim in the invite token that contains the tenant ID. | +| `Lobby.Frontend.BaseUrl` | `string` | Fallback redirect if the invite cannot continue directly into the organization. | + +## Requirements + +- The invite token must include the claim configured in `Invite.TenantClaim`. +- The request must resolve to the same tenant value after authentication. +- The invitation link should use the same host and route shape that the organization's normal + traffic uses so tenant resolution produces the expected value. + +## When to use another flow + +- If the invited user should create a new organization, use + [Invitation for Creating Organization](invitation-for-creating-organization.md). +- If the user should self-register and create an organization without an invite, use + [Registration](registration.md). diff --git a/Documentation/configuration/lobby/registration.md b/Documentation/configuration/lobby/registration.md new file mode 100644 index 0000000..0fbd1e2 --- /dev/null +++ b/Documentation/configuration/lobby/registration.md @@ -0,0 +1,51 @@ +# Registration + +Use registration when a user should create a new organization without receiving an invite first. +Users who are joining an existing organization should use an invitation flow instead. + +## Flow + +1. The user opens `https://your-authproxy/register`. +2. AuthProxy stores a short-lived HTTP-only registration cookie. +3. AuthProxy starts authentication. + - With one configured identity provider, AuthProxy challenges that provider directly. + - With multiple providers, AuthProxy redirects to the standard login page so the user can choose + a provider. +4. After a successful login, AuthProxy deletes the registration cookie. +5. AuthProxy redirects the user to `Invite.Lobby.Registration.BaseUrl`. + +The `/register` path is only active when `Invite.Lobby.Registration.BaseUrl` is configured. + +## Configuration + +```json +{ + "Cratis": { + "AuthProxy": { + "Invite": { + "Lobby": { + "Registration": { "BaseUrl": "https://lobby.example.com/register" } + } + } + } + } +} +``` + +| Property | Type | Description | +|----------|------|-------------| +| `Lobby.Registration.BaseUrl` | `string` | Absolute URL for the lobby registration experience that creates the organization after AuthProxy sign-in. | + +## Aspire + +When you configure AuthProxy through Aspire, set the registration destination with +`WithLobbyRegistration(...)`: + +```csharp +authproxy.WithLobbyRegistration(lobbyResource, "/register"); + +// or use a raw URL +authproxy.WithLobbyRegistration("https://lobby.example.com/register"); +``` + +This sets `Cratis:AuthProxy:Invite:Lobby:Registration:BaseUrl`. diff --git a/Documentation/configuration/lobby/toc.yml b/Documentation/configuration/lobby/toc.yml new file mode 100644 index 0000000..ed62b03 --- /dev/null +++ b/Documentation/configuration/lobby/toc.yml @@ -0,0 +1,8 @@ +- name: Lobby + href: index.md +- name: Invitation for Creating Organization + href: invitation-for-creating-organization.md +- name: Invitation to Organization + href: invitation-to-organization.md +- name: Registration + href: registration.md diff --git a/Documentation/configuration/tenancy.md b/Documentation/configuration/tenancy.md index 77c54fd..ece3fb8 100644 --- a/Documentation/configuration/tenancy.md +++ b/Documentation/configuration/tenancy.md @@ -201,7 +201,7 @@ For `Host`, `Claim`, and `Route`, AuthProxy resolves a source identifier and the ## Lobby fallback -When no tenant can be resolved and the [invite lobby](invites.md) is configured, AuthProxy redirects the user to the lobby frontend instead of returning `401 Unauthorized`. +When no tenant can be resolved and the [lobby](lobby/index.md) is configured, AuthProxy redirects the user to the lobby frontend instead of returning `401 Unauthorized`. If no lobby is configured and `TenantResolutions` is non-empty, AuthProxy returns `401 Unauthorized`. When `TenantResolutions` is empty, the request proceeds without a tenant. diff --git a/Documentation/configuration/toc.yml b/Documentation/configuration/toc.yml index 5a41e3b..c651e7d 100644 --- a/Documentation/configuration/toc.yml +++ b/Documentation/configuration/toc.yml @@ -8,8 +8,8 @@ href: tenant-selection.md - name: Services href: services.md -- name: Invites & Lobby - href: invites.md +- name: Lobby + href: lobby/toc.yml - name: Custom Invitation Provider-Selection Page href: invitation-provider-selection.md - name: Error Pages