# Quickstart: External Authentication This quickstart demonstrates the configuration-first broker with one OpenID Connect connection. Names are illustrative; exact extension methods follow [runtime-contracts.md](contracts/runtime-contracts.md). ## 1. Register Core Modules ```csharp services.AddElsa(elsa => { elsa.UseIdentity(identity => { identity.TokenOptions = options => { options.SigningKey = configuration["Identity:SigningKey"]!; options.Issuer = "https://elsa.example"; options.Audience = "https://elsa.example"; }; }); elsa.UseExternalAuthentication(feature => { feature.ConfigureOptions = options => configuration.GetSection("ExternalAuthentication").BindExternalAuthenticationOptions(options); }); }); services.AddOpenIdConnectExternalAuthentication(); ``` Configuration-only/single-node development uses the in-memory atomic state store. A production multi-node host also enables the External Authentication EF persistence feature (for example `SqliteExternalAuthenticationPersistence`) for shared external-authentication state, configures shared ASP.NET Core Data Protection keys, and gives every node the same External Authentication handle-hashing key: ```json { "ExternalAuthentication": { "HandleHashing": { "SharedKeyBase64": "" } } } ``` Generate the value with a cryptographically secure secret generator (for example, `openssl rand -base64 32`) and supply it through the deployment secret/configuration provider rather than source control. Rotating it invalidates outstanding broker transactions and changes persisted external-subject hashes, so rotation requires an explicit migration plan. Add the optional Elsa Secrets bridge whenever an Authentication Client or identity provider connection uses an `elsa-secrets` Secret Binding: ```csharp services.AddElsaSecretsExternalAuthentication(); ``` The protocol-neutral foundation includes a `configuration` resolver for External Secrets. It resolves a non-secret configuration key and derives a nonreversible generation fingerprint; Studio never receives the value. Use Elsa Secrets for Managed Secrets whose lifecycle is controlled through Elsa administration. ## 2. Register Authentication Clients ```json { "ExternalAuthentication": { "AuthenticationClients": [ { "clientId": "elsa-studio-server", "displayName": "Elsa Studio Server", "clientType": "confidential", "callbackUris": [ "https://studio.example/authentication/external/callback" ], "logoutCallbackUris": [ "https://studio.example/authentication/external/logout-callback" ], "allowedReturnPathPrefixes": ["/"], "secretBinding": { "resolverType": "elsa-secrets", "reference": "external-authentication/studio-server/client-secret", "expectedType": "text", "expectedScope": "external-authentication" }, "isEnabled": true }, { "clientId": "elsa-studio-wasm", "displayName": "Elsa Studio WebAssembly", "clientType": "public", "callbackUris": [ "https://studio-wasm.example/authentication/external/callback" ], "logoutCallbackUris": [ "https://studio-wasm.example/authentication/external/logout-callback" ], "allowedOrigins": [ "https://studio-wasm.example" ], "allowedReturnPathPrefixes": ["/"], "isEnabled": true } ] } } ``` Provision the confidential client secret as the active Elsa Secret named by `secretBinding.reference`, and supply the same value to Studio Server through its deployment secret or key vault. Never commit it: ```text Authentication__ExternalAuthentication__ClientSecret={strong-random-value} ``` ## 3. Configure an OpenID Connect Connection ```json { "ExternalAuthentication": { "Redirects": { "ExternalCallbackBaseUri": "https://elsa.example/elsa/api/" }, "Connections": [ { "id": "01JZCONTOSOOIDC000000000001", "key": "contoso-workforce", "adapterType": "openid-connect", "displayName": "Contoso", "iconId": "building", "displayOrder": 10, "isPreferred": true, "isEnabled": true, "adapterSettingsVersion": 2, "adapterSettings": { "mode": "discovery", "discoveryUrl": "https://login.contoso.example/.well-known/openid-configuration", "clientId": "elsa-server", "scopes": ["openid", "profile", "email", "groups"], "clientAuthenticationMethod": "client_secret_basic" }, "secretBindings": { "clientSecret": { "ownership": "external", "resolverType": "configuration", "reference": "ExternalAuthentication:Providers:Contoso:ClientSecret", "expectedType": "text", "expectedScope": "external-authentication" } }, "unlinkedPolicy": { "type": "create-user", "settingsVersion": 1, "settings": { "defaultRoleIds": ["workflow-user"] } }, "claimProjection": { "allowedClaimTypes": ["name", "email", "groups"], "redactedClaimTypes": ["email"], "maximumClaimCount": 50, "maximumValueLength": 2048, "maximumTotalBytes": 32768 }, "upstreamLogoutMode": "userChoice" } ], "Providers": { "Contoso": { "ClientSecret": "" } } } } ``` To manage the secret through Elsa instead, change the binding to: ```json { "ownership": "managed", "resolverType": "elsa-secrets", "reference": "external-authentication/contoso/client-secret", "expectedType": "text", "expectedScope": "external-authentication" } ``` Elsa derives the provider callback from its deployment-owned external base address and immutable logical Connection Key: ```text https://elsa.example/elsa/api/external-authentication/callback/contoso-workforce https://elsa.example/elsa/api/external-authentication/previews/callback/01JZCONTOSOOIDC000000000001 ``` Register both exact callbacks with the provider when administrators will use Preview. The first handles normal user sign-in and is keyed by the immutable logical Connection Key; the second handles administrator previews and is keyed by the stable connection record ID. Both are shown read-only in management responses and Studio. The callbacks, confidential-client requirement, S256 PKCE, and validation steps are immutable. Discovery-derived issuer, authorization/token endpoints, and signing keys appear only under **Advanced** when deployment policy enables unsafe provider trust and the caller has the dedicated permission; saving them requires explicit confirmation and leaves a persistent warning. The configuration-first example intentionally uses discovery without overrides. The role IDs in `defaultRoleIds` must exist, and the actor applying persisted equivalents must be authorized to assign them. They apply only when `create-user` creates a new user. The optional matcher-based policy selects one deployed `IExternalUserMatcher`; v1 ships no Elsa verified-email matcher, and matchers never select roles or permissions. ## 4. Configure Studio Server ```json { "Authentication": { "Provider": "ExternalAuthentication", "ExternalAuthentication": { "ClientId": "elsa-studio-server", "ClientSecret": "{from-secret-configuration}", "CallbackPath": "/authentication/external/callback", "LogoutCallbackPath": "/authentication/external/logout-callback" } } } ``` Host registration: ```csharp builder.Services.AddExternalAuthenticationBroker(options => configuration.GetSection("Authentication:ExternalAuthentication").Bind(options)); builder.Services.AddExternalAuthenticationModule(backendApiConfig); ``` The browser receives only the secure Studio cookie. Elsa access and refresh credentials stay in the Studio Server authentication session. ## 5. Configure Studio WebAssembly ```json { "Authentication": { "Provider": "ExternalAuthentication", "ExternalAuthentication": { "ClientId": "elsa-studio-wasm", "CallbackPath": "/authentication/external/callback", "LogoutCallbackPath": "/authentication/external/logout-callback", "BrowserStorage": "Memory" } } } ``` No client secret is configured. The default memory token store requires sign-in again after reload. `Session` or `Durable` browser storage is an explicit deployment choice that emits a security warning. ## 6. Verify the Broker 1. Request: ```http GET https://elsa.example/elsa/api/external-authentication/login-methods?clientId=elsa-studio-wasm ``` 2. Confirm `contoso-workforce` is returned as preferred without `discoveryUrl`, adapter settings, client ID, test details, remote icon, or secret data, and confirm Studio still shows the chooser. 3. Open Studio `/login`, select Contoso, and complete provider authentication. 4. Confirm the provider redirects only to Elsa's deployment-derived Connection Key callback. 5. Confirm Elsa redirects Studio with only an opaque completion code and client state. 6. Confirm code replay fails. 7. Confirm the Elsa access token has the session ID and permissions produced by the Elsa Roles assigned to the user. 8. Disable the connection and confirm initiation, pending callback, and external refresh fail while an already-issued access token follows its configured expiry. ## 7. Enable Persisted Administration Enable the `ExternalAuthenticationPersistence` shell feature and apply its migrations for the chosen provider. This is independent of Identity EF persistence: enabling `IdentityPersistence` alone leaves external authentication on the in-memory stores. The following become shared: - Database-owned connections and revisions. - External Identity Links. - External Authentication Sessions and rotating refresh state. - Broker transactions and completion grants. - Latest Connection Observations. Install `Elsa.Studio.Authentication.UI`, `Elsa.Studio.Settings`, and `Elsa.Studio.ExternalAuthentication`, then navigate to: ```text /settings/sso-connections ``` External Identity Links and External Authentication Sessions remain separate pages under `/security`. A configuration connection can be edited only by explicitly creating a complete Studio Override. Disabling that override keeps the configuration baseline shadowed; archiving it reveals configuration; restoring it resumes the shadow. Create a disabled draft, select OpenID Connect, configure fields and Secret Bindings, validate, test, preview, and enable. Configuration-owned connections remain inspect-only. Session administration is enabled by default and is available at `/security/external-authentication/sessions` to callers with the session read/revoke permissions. The ASP.NET Core health bridge is deliberately separate and opt-in: ```csharp services.AddExternalAuthenticationHealthCheck(); ``` Map it on a separately selected health endpoint/tag set. It is tagged `external-authentication` and `optional` and does not affect readiness unless the host explicitly includes it there. ## 8. Migrate from Direct Studio OpenID Connect Suppose Studio Server currently connects directly: ```json { "Authentication": { "Provider": "OpenIdConnect", "OpenIdConnect": { "Authority": "https://login.contoso.example", "ClientId": "studio-direct", "ClientSecret": "{deployment-secret}", "AuthenticationScopes": ["openid", "profile", "email"], "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-callback-oidc" } } } ``` Keep that section intact while adding the configuration-owned connection from section 3 and the confidential `elsa-studio-server` Authentication Client from section 2. Register Elsa's Connection-ID callback upstream, test the broker, then switch only: ```json { "Authentication": { "Provider": "ExternalAuthentication", "ExternalAuthentication": { "ClientId": "elsa-studio-server", "ClientSecret": "{separate-deployment-secret}", "CallbackPath": "/authentication/external/callback", "LogoutCallbackPath": "/authentication/external/logout-callback" } } } ``` For WebAssembly, the direct configuration similarly remains intact during rollout: ```json { "Authentication": { "Provider": "OpenIdConnect", "OpenIdConnect": { "Authority": "https://login.contoso.example", "ClientId": "studio-wasm-direct", "AuthenticationScopes": ["openid", "profile", "email"], "CallbackPath": "/authentication/login-callback", "SignedOutCallbackPath": "/authentication/logout-callback" } } } ``` Switch it to the public client from section 2: ```json { "Authentication": { "Provider": "ExternalAuthentication", "ExternalAuthentication": { "ClientId": "elsa-studio-wasm", "CallbackPath": "/authentication/external/callback", "LogoutCallbackPath": "/authentication/external/logout-callback", "BrowserStorage": "Memory" } } } ``` The WebAssembly client has no secret and always uses PKCE. Supply upstream and confidential broker secrets explicitly through their deployment secret configurations; never copy them through the API or UI. To roll back either host, restore `Authentication:Provider` to `OpenIdConnect` and restart; the retained direct settings were never changed. See [the migration guide](../../doc/migrations/external-authentication.md) for the complete setting map and retirement checklist. ## 9. Targeted Verification Commands Core: ```bash dotnet test test/unit/Elsa.ExternalAuthentication.UnitTests/Elsa.ExternalAuthentication.UnitTests.csproj dotnet test test/unit/Elsa.Identity.UnitTests/Elsa.Identity.UnitTests.csproj dotnet test test/integration/Elsa.ExternalAuthentication.IntegrationTests/Elsa.ExternalAuthentication.IntegrationTests.csproj dotnet test test/component/Elsa.Workflows.ComponentTests/Elsa.Workflows.ComponentTests.csproj dotnet build Elsa.sln ``` Studio: ```bash dotnet test src/modules/Elsa.Studio.ExternalAuthentication.Tests/Elsa.Studio.ExternalAuthentication.Tests.csproj dotnet build Elsa.Studio.sln ``` Run the browser suite against the deterministic fake provider for both Studio Server and WebAssembly before release.