14 KiB
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.
1. Register Core Modules
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:
{
"ExternalAuthentication": {
"HandleHashing": {
"SharedKeyBase64": "<base64-encoded random value of at least 32 bytes>"
}
}
}
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:
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
{
"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:
Authentication__ExternalAuthentication__ClientSecret={strong-random-value}
3. Configure an OpenID Connect Connection
{
"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": "<deployment-secret>"
}
}
}
}
To manage the secret through Elsa instead, change the binding to:
{
"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:
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
{
"Authentication": {
"Provider": "ExternalAuthentication",
"ExternalAuthentication": {
"ClientId": "elsa-studio-server",
"ClientSecret": "{from-secret-configuration}",
"CallbackPath": "/authentication/external/callback",
"LogoutCallbackPath": "/authentication/external/logout-callback"
}
}
}
Host registration:
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
{
"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
-
Request:
GET https://elsa.example/elsa/api/external-authentication/login-methods?clientId=elsa-studio-wasm -
Confirm
contoso-workforceis returned as preferred withoutdiscoveryUrl, adapter settings, client ID, test details, remote icon, or secret data, and confirm Studio still shows the chooser. -
Open Studio
/login, select Contoso, and complete provider authentication. -
Confirm the provider redirects only to Elsa's deployment-derived Connection Key callback.
-
Confirm Elsa redirects Studio with only an opaque completion code and client state.
-
Confirm code replay fails.
-
Confirm the Elsa access token has the session ID and permissions produced by the Elsa Roles assigned to the user.
-
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 <Provider>ExternalAuthenticationPersistence shell feature and apply its migrations for the chosen provider. This is independent of Identity EF persistence: enabling <Provider>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:
/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:
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:
{
"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:
{
"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:
{
"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:
{
"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 for the complete setting map and retirement checklist.
9. Targeted Verification Commands
Core:
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:
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.