elsa-core/specs/012-external-authentication/tasks.md
Sipke Schoorstra 74123110d5
feat(auth)!: structured authorization model, phases 1-6 (#7980)
* feat(auth): add the permission model and evaluator (Phase 1)

Additive only. Nothing changes behavior: no endpoint declares against this
yet, and no existing enforcement path routes through it.

A permission is {resource}:{verb}, both axes open and string-keyed. A
trailing wildcard on the resource axis matches the named node and every
descendant at any depth, so workflows/definitions/* covers
workflows/definitions itself; * on the verb axis matches any verb.
Wildcards are the only construct with forward reach.

A bare * parses to *:* at parse time rather than being special-cased in
the evaluator, so superuser stays an ordinary grant and a stored or seeded
* keeps authorizing across the vocabulary migration without a lock-out
window.

Adds:
- Permission, with parsing that rejects a value containing a comma, since
  the persistence converter joins collections with one
- CoreVerbs, the recommended set modules should reuse; a convention rather
  than a closed vocabulary
- PermissionMatcher, one matching rule shape on both axes
- IPermissionEvaluator, the single place permission decisions are made,
  skipping malformed claims so one bad stored grant cannot deny a principal
- PermissionRequirement and PermissionAuthorizationHandler
- The descriptor catalog in core: PermissionDescriptor now carries the
  verbs a resource supports and marks non-core ones, and the registry can
  report what a wildcard covers today

External Authentication keeps its own descriptor types for now; it moves to
the core catalog with the other modules in Phase 2, which keeps this change
purely additive.

55 unit tests cover the matcher table, wildcard forward reach, the
counterpart that concrete grants stay frozen, absence-is-denial, and the
seeded * case.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(auth): contribute the permission catalog from every module (Phase 2)

Still additive. Existing endpoints keep their legacy declarations; nothing
changes behavior for them.

Every module exposing protected endpoints now declares its resources and
the verbs each accepts, following the pattern already proven in External
Authentication -- constants and descriptors colocated -- refined to one
constant per resource, with the verb supplied separately. 47 resources
across 15 modules, matching the settled vocabulary.

Descriptors are discovered from the same assemblies as a module's
endpoints, in AddFastEndpointsFromModule. Registering them per module
would let the catalog and the endpoints drift, which is the failure this
model exists to remove; tying them to one registration makes the catalog
necessarily describe the endpoints that exist.

Adds:
- GET /identity/permissions, the catalog a role editor renders from, so
  no client hard-codes permission strings
- GET /identity/permissions/reach, reporting what a wildcard covers today.
  This is the mitigation for forward reach on the resource axis: a
  wildcard is useful precisely because it covers things that do not exist
  yet, so an author needs to see what it reaches now
- GET /identity/me/permissions, resolving wildcards to concrete verbs so a
  client needs no matching logic, and listing denied resources with an
  empty verb list so "denied" is distinguishable from "unknown"
- IPermissionGrantValidator, wired into Roles/Create and Roles/Update,
  which previously persisted request.Permissions after only the
  caller-subset check. Concrete segments validate against the catalog;
  wildcards validate structurally and are accepted even when they match
  nothing today, since installing a module later is what gives such a
  grant meaning
- RequirePermission(resource, verb) and RequireAuthenticatedOnly() on the
  endpoint base classes, with the six copy-pasted ConfigurePermissions
  bodies collapsed into one implementation

New endpoints require new-format grants, so during the transition they
authorize only for holders of *, which parses to *:*. Phase 3 migrates the
rest and closes that gap.

70 unit tests, including the wildcard-accepting validator cases.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(auth)!: cut every endpoint over to the permission model (Phase 3)

BREAKING: legacy permission strings no longer authorize. A permanent alias
layer would keep two vocabularies valid forever, so the break is
deliberate and reported rather than absorbed. `*` survives unchanged --
it parses to `*:*` -- so an administrator cannot be locked out while
roles are re-authored.

All 168 declaration call sites across 151 files now use
RequirePermission(resource, verb) with the constants their module
declares, so a typo is a compile error rather than an unreachable
endpoint.

Enforcement consolidated onto IPermissionEvaluator:
- RoleAuthorizationService evaluates containment through the evaluator
  rather than by set membership. This matters: a caller holding
  workflows/*:view can now delegate workflows/definitions:view, which set
  membership got wrong and which would otherwise force administrators to
  hold every concrete grant they wish to delegate.
- The two Broker/Logout.cs endpoints declare explicitly. Logout is
  authenticated-only; ContinueLogout is anonymous, matching every other
  broker callback -- the route handle carries the authority and a
  top-level browser navigation sends no Authorization header.

Removes the C#/Python expression permissions (#7975). They conflated an
incoherent execution-side gate -- a workflow runs under the server's
authority, not the caller's, so the check never constrained what a script
could do -- with a meaningful authoring-side one. The host switch
(AllowHostCodeExecution) becomes the single control. This is a deliberate
reduction in control: where host code is enabled, any author who may write
definitions may use C# and Python.

Adds the fail-closed gate. Omitting a declaration previously inherited the
FastEndpoints default with no Elsa-level fallback, so an endpoint could
ship ungated unnoticed. EndpointCoverage asserts every endpoint declares
exactly one of RequirePermission, RequireAuthenticatedOnly or
AllowAnonymous, with no exemption list. Its canary assertion earned its
keep immediately by catching that the gate was scanning an assembly
containing no endpoints.

EndpointPermissionRegistry records what each endpoint declares. The
requirement is attached as an inline policy and is not readable back from
the definition, so this keeps the declaration introspectable -- and lets
tests assert a specific requirement rather than merely that one exists.

Two behavior notes worth calling out:
- The runtime status endpoint previously accepted either the read or the
  manage permission. It now requires workflows/runtime:view alone, which
  is least privilege; a role holding only control must also be granted
  view to read status.
- BPMN interchange repeats the workflow-definitions path locally rather
  than taking a dependency on Elsa.Workflows.Api for one constant. It
  contributes no descriptor: the resource is owned and described by
  Workflows.Api, and the registry keeps one entry per resource.

Also adds a startup validator that logs every stored role permission that
no longer resolves, identified by role, so an upgrade is loud.

188 unit tests pass across Api.Common, Workflows.Api and Identity.

Refs #7974, #7975, #7976

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(auth): revocation bound and role audit notifications (Phase 4)

Default access-token lifetime drops from 1 hour to 15 minutes. This is
the revocation bound: permission claims are issued at sign-in and refresh
re-reads the user's roles, so removing a role takes effect at most one
access-token lifetime later. Refresh already rotates both tokens, so no
client change is required and the refresh lifetime is unchanged.

Adds an optional permission stamp for deployments needing a tighter
bound. The stamp is derived from the user's roles and their permissions
rather than stored as a counter on the user. That avoids changing the
Identity schema, which would have required migrations across all five EF
providers and made this milestone depend on the tenancy work. It also
means every node computes the same value from the same store with no
cross-node cache invalidation, which matters because Elsa has none.

The stamp is issued unconditionally and only validated when enabled, so
turning it on does not invalidate tokens already in flight; an absent
stamp is not treated as a mismatch for the same reason. It changes when a
role is added to or removed from the user and when a held role's
permissions change, but not when an unrelated role changes.

Role create and update now publish typed security notifications per ADR
0007, carrying the resulting grants so a reviewer can reconstruct what a
role conferred at a point in time without replaying every prior event.
This module owns no audit store: a future audit module subscribes and
sets its own retention.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(auth): tenancy hardening for identity (Phase 5)

Closes the gaps that made "roles are configurable per tenant" untrue
however the rest of the stack behaved.

Uniqueness becomes per tenant. User.Name, Role.Name, Application.Name and
Application.ClientId carried globally unique indexes, so two tenants could
not both hold a role named Admin. Migrations for all five EF providers
drop the global indexes and create composite ones on (TenantId, Name).

The in-memory user and role stores now scope to the ambient tenant.
Isolation previously existed only on the Entity Framework path, and only
when multitenancy was enabled, so a deployment running the default stores
had none at all. The tenant-agnostic sentinel is honored, matching the EF
query filter, so a shared platform role stays visible from every tenant.

RoleFilter gains TenantId, matching UserFilter, and the role and user list
endpoints pass it explicitly rather than relying on an ambient filter that
only exists on one persistence path.

UserManager.CreateUserAsync sets TenantId explicitly instead of relying on
the EF saving handler, which does not run in memory and left users
unassigned there.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: migration guide, ADR, and security wiki for the authorization model

Adds docs/migrations/authorization-model.md, following the shape of the
external-authentication persistence guide. It leads with the three things
that are not a simple rename, because each silently produces a wrong
result if treated as one:

- The migration expands where new sub-resources are finer-grained than
  what they replace, so a one-for-one substitution narrows roles.
- read:* and exec:* become materially more powerful. They are literal
  claim values today, authorizing twelve of roughly forty read endpoints;
  their replacements work as the names always implied. Any role holding
  them needs review by hand, not an automated rewrite.
- The C#/Python expression permissions are removed rather than
  translated, which is a deliberate reduction in control where host code
  is enabled.

It also states plainly that `*` keeps working, and says to do that first,
since it is what stops an instance locking itself out mid-migration.

ADR 0012 records the model and, more usefully, why a closed verb
enumeration was drafted and rejected: it was justified on implication, but
aggregates were already excluded and no verb implies another, so the
bitwise check was expressing set containment all along.

The security wiki's API Authorization section replaces its Secrets-only
route table with the catalog endpoint as the authoritative source, and
states why read-only mode is a separate axis rather than a permission.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(auth): restore the suites after the tenancy and evaluator changes

The whole solution builds with zero errors and every affected suite
passes: 70 Api.Common, 23 Workflows.Api, 95 Identity, 154 External
Authentication unit, 133 External Authentication integration.

Most breakage was test call sites constructing the tenant-aware stores
and the evaluator-backed RoleAuthorizationService directly. Adds
TestTenantAccessor to Elsa.Testing.Shared rather than giving the
production constructors an optional accessor, which would have let a
missing registration silently disable isolation.

Several External Authentication tests created fixtures in tenant-a while
running under the default tenant, so the newly isolating store correctly
stopped finding them. They are now scoped to the tenant their own
fixtures use; JustInTimeProvisioningTests, which genuinely spans two
tenants, is scoped per case.

One production fix came out of it: IdentityFeature now ensures an
ITenantAccessor with TryAdd. The identity stores are tenant-scoped, so a
host that never enables multitenancy would otherwise fail to construct
them -- which is what the DI registration tests were reporting. TryAdd
leaves MultitenancyFeature's own registration untouched.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(auth): register the identity services on the classic feature path

Found by running Elsa.Server.Web, not by the test suites: the app failed
at startup with "Unable to resolve service for type RoleSecurityNotifier
while attempting to activate Roles.Update".

RoleSecurityNotifier, the permission stamp services, the memory cache and
the stored-permission validator were registered only in the CShells shell
feature. Elsa.Server.Web uses the classic UseIdentity() path, whose
IdentityFeature registered none of them, so every host on that path
crashed while mapping endpoints. Unit tests did not catch it because they
construct services directly rather than through either feature.

Verified end to end against the running server:

- The seeded admin role stores "*". It parsed to *:* and resolved to
  concrete verbs across all 27 registered resources, which is the
  bare-wildcard parse rule working on real data rather than in a test.
- GET /identity/permissions returns the catalog for the modules this app
  installs -- 27 resources, 0 unverified, categories Dashboard, Identity,
  Resilience and Workflows -- rather than all 47, which is correct: the
  catalog describes what is installed.
- GET /identity/permissions/reach?resource=workflows/* reports 19 covered
  resources.
- A role holding only dashboard:view gets 200 on /dashboard/overview and
  403 on /identity/roles, /identity/users, /workflow-definitions and
  /identity/permissions, while /identity/me/permissions returns 200
  because it declares RequireAuthenticatedOnly -- confirming FR-019's
  third declaration state behaves as designed.
- That same principal's /me/permissions lists all 27 resources with 26
  carrying an empty verb list, so "denied" stays distinguishable from
  "unknown to this server".
- The startup validator logged no unresolvable permissions, as expected
  for a seed holding only "*".

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(auth): discover permission descriptors on the shell host path

Found by running Elsa.ModularServer.Web. The shell host started cleanly
and authorized correctly, but GET /identity/permissions returned zero
resources and /identity/me/permissions returned no grants.

Descriptor discovery was wired into AddFastEndpointsFromModule, which only
the classic module path calls. CShells discovers endpoints from features
implementing its own marker interface, so on a shell host no provider was
ever registered. Authorization still worked, because the evaluator reads
claims and needs no descriptors -- which is exactly why nothing failed
loudly. What silently broke was everything built on the catalog: role
authoring would have rejected every concrete grant as an unknown
resource, introspection returned nothing for clients to render, and the
stored-permission validator would have reported every concrete stored
permission as unresolvable.

ElsaFastEndpointsFeature now contributes descriptors from the loaded Elsa
assemblies, bounded to those and run once per shell.

Verified on the modular host, which installs far more modules than
Elsa.Server.Web:

- 47 resources registered, 0 unverified, across all 12 categories, with
  all 17 module-specific verbs present. That is the entire published
  vocabulary confirmed against a running server rather than a document.
- Reach reports workflows/* covering 20, external-authentication/*
  covering 8, and * covering 47.
- Creating a role with dashboard:view and workflows/*:view succeeds,
  confirming a wildcard grant survives authoring validation.
- Creating one with invented/resource:view and secrets:publish is
  rejected with 400.

Also makes those rejections actionable. The permission was reported
without the reason, so an operator learned which entry was wrong but not
why; both parts are now in the message, including the supported verbs for
the resource.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* wip: bpmn test vocabulary

* fix(auth): make enforcement DI-independent and finish the hub cutover

CI on #7980 was red. Running the full suite locally rather than the
subset I had been checking surfaced 24 failures across four projects,
in three distinct classes.

Enforcement no longer depends on a DI registration. RequirePermission
attached a PermissionRequirement evaluated by a registered handler, so a
host that had not called AddElsaAuthorization got 403 on every endpoint
with nothing to indicate why. Several test hosts wire FastEndpoints
directly and did exactly that. The requirement is now evaluated inline
against a shared stateless evaluator, with a host-registered
IPermissionEvaluator still taking precedence. Registration remains
worthwhile for the catalog and the validator; authorization can no longer
silently fail closed because of a missing one.

Registration also moved from AddFastEndpointsFromModule to
AddFastEndpointsAssembly. Registering an endpoint assembly is what should
guarantee its permissions work, and a host may never call the former.

Finishes T039. The four SignalR hubs still matched hard-coded legacy
permission strings, which no longer exist, so every hub denied access.
They now route through the evaluator like every other enforcement path.

Test fixtures granting legacy strings were updated to the new vocabulary.
Two categories were deliberately left alone: naming tests asserting the
legacy constants still hold their old values, which is true and worth
keeping, and the workflow script authorization tests, which asserted a
MissingPermission outcome that D21 removed -- those now assert the host
switch is the only control.

One test previously pinned that the hub honors a FastEndpoints-configured
permissions claim type. It now asserts the opposite, and says why: Elsa is
the only authority that expands roles into permission claims (ADR 0009),
and this model no longer uses the FastEndpoints permission mechanism, so
its separately configurable claim type is not consulted. That property is
also unreadable outside reflection.

Whole solution builds with 0 errors and every test project passes.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(auth): scope the permission-stamp cache to the tenant

Greptile found and reproduced a cross-tenant authorization bug, and it was
mine: Phase 5 made user names unique per tenant rather than globally, but
PermissionStampValidator kept caching by user name alone. Tenant A's
lookup could therefore populate the cache with its own stamp and satisfy a
revoked token belonging to a same-named user in tenant B, without ever
resolving tenant B's user.

Both the cache key and the user lookup are now tenant-scoped. Added
PermissionStampValidatorTests, including the cross-tenant case; verified it
fails without the fix and passes with it.

Also from review:
- Removed the legacy permission constants left unused in the three hubs
  after they moved to the evaluator, so no stale vocabulary lingers.
- Narrowed two generic catch clauses. The IL scanner now catches only the
  exceptions an unresolvable metadata token actually throws, and the
  startup validator rethrows cancellation while still refusing to stop the
  host for anything else -- an unreachable or half-migrated store is
  exactly when an operator most needs the host up.

Whole solution builds with 0 errors and every test project passes.

Refs #7974

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(docs): correct path in log message for authorization model migration link

Aligns the log message path to the correct documentation directory, changing `docs` to `doc` to avoid confusion and incorrect linking during log output.

* fix(auth): update permissions method to use new syntax

* docs: consolidate docs/ into doc/

The repository had two documentation roots. Merge docs/ into doc/ and
remove the empty docs/ tree.

The two adr/ folders both numbered from 0001, so the identity and
authorization series is renumbered to continue the core series rather
than collide with it:

  docs/adr/0001-0012 -> doc/adr/0014-0025

Every reference is updated to match: the Status cross-links between the
renumbered ADRs, the ADR and path links in specs/012-external-authentication
and specs/013-rbac-authorization-model, and doc/wiki/identity-tenancy-security.md.

doc/adr/toc.md gains entries 14-25. doc/adr/graph.dot is regenerated out
to 25; it had been stale since ADR 10 and now also carries the partial
supersession edges declared by the ADRs themselves.

docs/codebase/ and docs/migrations/ move across unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(auth): simplify syntax in PermissionEvaluator and related classes

Streamlined syntax for method definitions by using expression-bodied members and simplified object instantiations across the Authorization module. This includes adjustments in `PermissionEvaluator`, `LocalHostRequirement`, and `WebApplicationExtensions` for better readability and maintainability.

* ci(bounty): point the footer step at the file's real path

The bounty workflow read docs/bounty-footer.md, the path the file had
when the workflow was added in b421b00e1. The file later moved to
doc/bounty/bounty-footer.md and the workflow was never updated, so the
read step has been resolving nothing and the appended comment was empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 23:44:55 +02:00

49 KiB
Raw Blame History

Tasks: External Authentication

Input: Design documents from /specs/012-external-authentication/ Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/, and quickstart.md

Tests: Tests are required by the specification and are written before the corresponding implementation.

Organization: Tasks are grouped by user story. Core paths are relative to the elsa-core repository; paths beginning with /Users/sipke/Projects/Elsa/elsa-studio/ target the sibling Studio repository.

Revision note (2026-07-24): T001T116 record the completed baseline against the earlier specification. They remain checked as historical evidence; they do not imply that the approved revision is complete. T117 onward is the authoritative open delta and must pass before release readiness is claimed.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel because it touches different files and has no dependency on another incomplete task in the same phase.
  • [Story]: Maps the task to one of the user stories in spec.md.

Phase 1: Setup

Purpose: Establish the Core and Studio project boundaries, references, test hosts, and dependency declarations.

  • T001 Create src/modules/Elsa.ExternalAuthentication/Elsa.ExternalAuthentication.csproj, register it in Elsa.sln, and add the protocol-neutral module folders defined by plan.md.
  • T002 [P] Create src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Elsa.ExternalAuthentication.OpenIdConnect.csproj, register it in Elsa.sln, and reference the broker project.
  • T003 [P] Create src/modules/Elsa.ExternalAuthentication.Secrets/Elsa.ExternalAuthentication.Secrets.csproj, register it in Elsa.sln, and reference the broker and Secrets abstractions.
  • T004 [P] Create test/unit/Elsa.ExternalAuthentication.UnitTests/Elsa.ExternalAuthentication.UnitTests.csproj and register it in Elsa.sln.
  • T005 [P] Create test/integration/Elsa.ExternalAuthentication.IntegrationTests/Elsa.ExternalAuthentication.IntegrationTests.csproj with shared Identity EF and web-host fixtures and register it in Elsa.sln.
  • T006 [P] Add the maintained IdentityModel protocol dependencies and central versions required by the OpenID Connect adapter in Directory.Packages.props.
  • T007 [P] Create /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Elsa.Studio.ExternalAuthentication.csproj and register it in /Users/sipke/Projects/Elsa/elsa-studio/Elsa.Studio.sln.
  • T008 [P] Create /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorServer/Elsa.Studio.ExternalAuthentication.BlazorServer.csproj and /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorWasm/Elsa.Studio.ExternalAuthentication.BlazorWasm.csproj and register both in /Users/sipke/Projects/Elsa/elsa-studio/Elsa.Studio.sln.
  • T009 [P] Create /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Elsa.Studio.ExternalAuthentication.Tests.csproj and /Users/sipke/Projects/Elsa/elsa-studio/tests/browser/ExternalAuthentication/README.md, including the component and Playwright test dependencies.

Phase 2: Foundational

Purpose: Implement the shared contracts and security boundaries that block every independently deliverable user story.

Critical: No user-story implementation starts until this phase passes its unit tests.

  • T010 [P] Define connection envelopes, source/scope/lifecycle enums, material revision inputs, claim projections, external identities, authentication clients, broker transactions, completion grants, sessions, and observations in src/modules/Elsa.ExternalAuthentication/Models/ covering FR-001FR-016, FR-020FR-022, and FR-035FR-043.
  • T011 [P] Define adapter, descriptor, registry, connection source, connection store, policy, grant source, secret resolver, atomic state, observation, link provisioning, and token service interfaces in src/modules/Elsa.ExternalAuthentication/Contracts/ covering FR-017FR-031 and FR-049FR-065.
  • T012 [P] Define deployment options and secure defaults for clients, sources, policies, storage, lifetimes, claims, rate limits, egress, redirects, WebAssembly persistence, logout, and lockout guard in src/modules/Elsa.ExternalAuthentication/Options/ covering FR-036, FR-042, FR-048, FR-058, FR-070FR-078, and FR-092FR-103.
  • T013 [P] Define fine-grained permission constants and permission descriptors in src/modules/Elsa.ExternalAuthentication/Permissions/ExternalAuthenticationPermissions.cs covering FR-030, FR-063FR-064, FR-082, and FR-085.
  • T014 [P] Define immutable redacted security notification records in src/modules/Elsa.ExternalAuthentication/Notifications/ covering FR-096 and FR-100FR-101.
  • T015 Implement startup validation for unique installed adapter, policy, grant-source, and client identifiers plus exact callback/origin/logout registrations in src/modules/Elsa.ExternalAuthentication/Validation/ExternalAuthenticationOptionsValidator.cs covering FR-017FR-019, FR-036, FR-075, and FR-077FR-078.
  • T016 Implement configuration-owned connection loading, validation, immutable IDs, and canonical material revisions in src/modules/Elsa.ExternalAuthentication/Providers/ConfigurationIdentityProviderConnectionSource.cs and Services/ConnectionRevisionCalculator.cs covering FR-001FR-003, FR-006FR-009, FR-020, FR-039, and FR-098.
  • T017 Implement the merged effective connection registry with source precedence, collision rejection, shadow diagnostics, tenant isolation, deterministic ordering, and version barriers in src/modules/Elsa.ExternalAuthentication/Services/DefaultIdentityProviderConnectionRegistry.cs covering FR-004FR-010, FR-016, FR-041, and FR-071.
  • T018 [P] Implement atomic in-memory broker transaction, completion grant, session, preview, observation, and version-barrier stores for single-node use in src/modules/Elsa.ExternalAuthentication/Stores/InMemory/ covering FR-034FR-046 and FR-086FR-090.
  • T019 Refactor reusable Elsa access/refresh token construction behind IElsaTokenService while preserving IAccessTokenIssuer and existing token contracts in src/modules/Elsa.Identity/Contracts/IElsaTokenService.cs and src/modules/Elsa.Identity/Services/DefaultElsaTokenService.cs covering FR-034, FR-045, FR-060, and FR-073.
  • T020 Make local credentials optional without placeholder hashes and preserve indistinguishable invalid-login behavior in src/modules/Elsa.Identity/Entities/User.cs and src/modules/Elsa.Identity/Services/DefaultUserCredentialsValidator.cs covering FR-052FR-053.
  • T021 Wire the protocol-neutral feature, shell feature, services, FastEndpoints groups, rate limiting, and Data Protection purposes in src/modules/Elsa.ExternalAuthentication/Features/ExternalAuthenticationFeature.cs and ShellFeatures/ExternalAuthenticationShellFeature.cs.
  • T022 [P] Add foundational unit tests for options validation, material revisions, registry merge/tenant rules, atomic single-use stores, redaction, and local credential compatibility in test/unit/Elsa.ExternalAuthentication.UnitTests/Foundational/ and test/unit/Elsa.Identity.UnitTests/ExternalAuthentication/.
  • T023 [P] Add safe public error categories, correlation IDs, allowlisted local return paths, and shared response redaction helpers in src/modules/Elsa.ExternalAuthentication/Services/BrokerErrorFactory.cs, Validation/ClientReturnPathValidator.cs, and Services/ExternalAuthenticationRedactor.cs covering FR-078 and FR-094FR-097.

Checkpoint: The broker has stable protocol-neutral contracts, secure configuration, source composition, atomic single-node primitives, and compatible Elsa credential issuance.


Phase 3: User Story 1 - Sign In Through an External Provider (Priority: P1) — MVP

Goal: Let a user discover an enabled OpenID Connect connection, authenticate upstream, resolve or JIT-provision an Elsa user, and receive Elsa credentials through a PKCE-bound completion code.

Independent Test: Configure one connection and one client entirely in configuration, run discovery → authorize → provider callback → token exchange against the deterministic fake provider, and verify one Elsa user/link/session plus usable Elsa permissions.

Tests for User Story 1

  • T024 [P] [US1] Add OpenID Connect adapter conformance and validation tests for code flow, state, nonce, signature, issuer, audience/authorized-party, expiry, callback errors, optional upstream PKCE, and normalized claims in test/unit/Elsa.ExternalAuthentication.UnitTests/OpenIdConnect/OpenIdConnectAdapterTests.cs covering FR-022FR-025 and SC-001.
  • T025 [P] [US1] Add broker endpoint contract tests for discovery, initiation, callback, local authorize, code exchange, and logout in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Broker/BrokerContractTests.cs covering FR-032FR-048, FR-066FR-073, and SC-001.
  • T026 [P] [US1] Add replay, exact-callback, PKCE, revision-change, disabled/archive, tenant enumeration, and redirect-allowlist security tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Security/BrokerSecurityTests.cs covering FR-033FR-041, FR-046, FR-077FR-078, FR-094FR-095, and SC-007/SC-011.
  • T027 [P] [US1] Add credential-less JIT concurrency and local-login indistinguishability tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Identity/JustInTimeProvisioningTests.cs covering FR-049FR-057 and SC-009.

Implementation for User Story 1

  • T028 [P] [US1] Implement the OpenID Connect adapter descriptor, versioned settings, discovery/manual trust validation, and adapter registration in src/modules/Elsa.ExternalAuthentication.OpenIdConnect/ covering FR-018FR-024 and FR-029.
  • T029 [US1] Implement hardened provider authorization and callback processing with maintained protocol primitives and normalized claim projection in src/modules/Elsa.ExternalAuthentication.OpenIdConnect/Services/OpenIdConnectExternalAuthenticationAdapter.cs covering FR-022FR-024 and FR-092FR-099.
  • T030 [P] [US1] Implement reject and JIT unlinked-identity policies in src/modules/Elsa.ExternalAuthentication/Policies/RejectUnlinkedIdentityPolicy.cs and Policies/CreateUserUnlinkedIdentityPolicy.cs covering FR-049FR-058.
  • T031 [US1] Implement atomic link resolution and credential-less create-link-or-get-existing provisioning in src/modules/Elsa.ExternalAuthentication/Services/DefaultExternalIdentityResolver.cs covering FR-049FR-057.
  • T032 [US1] Implement Login Method discovery and external/local initiation endpoints in src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/GetLoginMethods.cs, AuthorizeExternal.cs, and AuthorizeLocal.cs covering FR-016, FR-032, FR-035FR-041, and FR-066FR-073.
  • T033 [US1] Implement provider callback processing and single-use PKCE-bound completion grants in src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/HandleCallback.cs and Services/ExternalAuthenticationBroker.cs covering FR-032FR-041 and FR-049FR-057.
  • T034 [US1] Implement authorization-code exchange, rotating external refresh, session checks, and additive local broker completion in src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/ExchangeToken.cs covering FR-034FR-046 and FR-072FR-076.
  • T035 [US1] Implement Elsa logout plus Disabled/UserChoice/Always upstream logout behavior in src/modules/Elsa.ExternalAuthentication/Endpoints/Broker/Logout.cs covering FR-047FR-048 and FR-077.

Checkpoint: Configuration-first external and opt-in brokered local sign-in work end to end without persisted administration.


Phase 4: User Story 2 - Manage Persisted Connections (Priority: P1)

Goal: Let authorized administrators create, inspect, edit, enable, disable, archive, restore, and delete eligible database-owned connections without restarting Elsa.

Independent Test: Create a disabled draft through the API, complete its fields and Secret Bindings, enable it, observe it in discovery immediately on a second node, update with ETags, archive/restore it, and verify configuration-owned entries remain read-only.

Tests for User Story 2

  • T036 [P] [US2] Add CRUD, draft/enable, archive/restore, source-ownership, collision, ETag, and stale-registry contract tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Connections/ConnectionManagementTests.cs covering FR-001FR-016 and SC-002/SC-012.
  • T037 [P] [US2] Add EF persistence and migration tests for all entities, unique indexes, concurrency tokens, concurrent JIT convergence and compensation, and authoritative registry versions in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Persistence/ExternalAuthenticationPersistenceTests.cs covering FR-011FR-012, FR-040FR-041, FR-055, and SC-006.
  • T038 [P] [US2] Add Studio connection list/editor component tests for ownership, lifecycle, validation, secret configured-state, unsafe-setting warnings, and allowed actions in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Connections/ConnectionEditorTests.cs covering FR-014, FR-026FR-030, FR-082FR-085, and SC-002.

Implementation for User Story 2

  • T039 [P] [US2] Add persisted connection, policy/grant selections, claim projection, link, broker transaction, grant, session, observation, and preview entities/configurations to src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/ExternalAuthenticationElsaDbContext.cs and Configurations.cs covering the data-model.md persistence model.
  • T040 [US2] Implement EF connection, registry-version, atomic state, session, observation, preview, and atomic identity-link stores in src/modules/Elsa.ExternalAuthentication.Persistence.EFCore/Stores/ covering FR-003, FR-011FR-012, FR-040FR-041, FR-055, and FR-090.
  • T041 [P] [US2] Generate External Authentication migrations and snapshots for SQLite, SQL Server, PostgreSQL, MySQL, and Oracle in src/modules/Elsa.ExternalAuthentication.Persistence.EFCore.{Sqlite,SqlServer,PostgreSql,MySql,Oracle}/Migrations/ExternalAuthentication/.
  • T042 [US2] Implement create/read/update/enable/disable/archive/restore/delete endpoints with ETags, source ownership, collision semantics, validation, and security notifications in src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/ covering FR-001FR-015, FR-082, FR-085, and FR-100.
  • T043 [P] [US2] Implement descriptor, permission-descriptor, and policy/grant-source catalog endpoints in src/modules/Elsa.ExternalAuthentication/Endpoints/Descriptors/ covering FR-017FR-019 and FR-064.
  • T044 [P] [US2] Implement the optional Elsa Secrets resolver with generation fingerprints and no-reveal replacement/removal semantics in src/modules/Elsa.ExternalAuthentication.Secrets/Services/ElsaSecretBindingResolver.cs covering FR-026FR-028 and FR-098.
  • T045 [P] [US2] Add typed External Authentication resources and Refit clients in src/clients/Elsa.Api.Client/Resources/ExternalAuthentication/ for every management and descriptor endpoint in contracts/rest-api.md.
  • T046 [US2] Implement the Studio Security menu and paginated connection list with source, scope, enabled/valid/test states, shadowing, and caller-authorized actions in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Menu/ExternalAuthenticationMenu.cs and Pages/Connections/Index.razor covering FR-080, FR-082FR-085.
  • T047 [US2] Implement the schema-driven Studio connection editor, lifecycle dialogs, Secret Binding controls, concurrency recovery, and unsafe trust confirmation in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Pages/Connections/Edit.razor and Components/ConnectionEditor/ covering FR-014, FR-018FR-021, FR-026FR-030, and FR-083.

Checkpoint: Persisted connection administration is complete and cross-node discovery reflects committed mutations.


Phase 5: User Story 3 - Preserve Elsa Authorization (Priority: P1)

Goal: Resolve every authenticated external identity to an Elsa user and compose only Elsa-authorized permissions from explicit, bounded grant sources.

Independent Test: Sign in with mapped and unmapped external claims, inspect Preview Sign-in provenance, modify Elsa roles, refresh the external session, and verify unmapped/unauthorized permissions never appear while current Elsa-owned grants do.

Tests for User Story 3

  • T048 [P] [US3] Add permission pipeline unit tests for source composition, deterministic deduplication, provenance, unknown descriptors, allow/deny boundaries, and unmapped claims in test/unit/Elsa.ExternalAuthentication.UnitTests/Permissions/PermissionGrantPipelineTests.cs covering FR-060FR-065 and SC-008.
  • T049 [P] [US3] Add delegation authorization tests proving ordinary administrators cannot grant permissions they lack while unrestricted delegates remain deployment-bounded in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Permissions/PermissionDelegationTests.cs covering FR-063FR-064 and SC-008.
  • T050 [P] [US3] Add external refresh tests proving snapshots remain bounded while current Elsa user/role grants are reevaluated in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Sessions/ExternalRefreshPermissionTests.cs covering FR-042FR-045 and FR-060FR-065.

Implementation for User Story 3

  • T051 [P] [US3] Implement Elsa user/role and explicit external claim/group Permission Grant Sources in src/modules/Elsa.ExternalAuthentication/Permissions/ covering FR-060FR-065.
  • T052 [US3] Implement ordered grant-source composition, permission provenance, deployment boundaries, and delegation checks in src/modules/Elsa.ExternalAuthentication/Services/DefaultPermissionGrantResolver.cs covering FR-060FR-064.
  • T053 [P] [US3] Add the optional module-contributed permission descriptor provider/registry in src/modules/Elsa.ExternalAuthentication/Services/DefaultPermissionDescriptorRegistry.cs covering FR-064.
  • T054 [US3] Integrate resolved permission grants and provenance with Elsa token issuance and external-session snapshots in src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationBroker.cs covering FR-042FR-045 and FR-060FR-065.
  • T055 [P] [US3] Implement permission mapping and boundary controls in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Components/PermissionMappings/ covering FR-061FR-064.
  • T056 [US3] Implement Preview Sign-in permission provenance and descriptor warnings in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Components/Preview/PermissionPreview.razor covering FR-064 and FR-086FR-088.
  • T057 [US3] Add permission preview and delegation component tests in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Permissions/PermissionMappingTests.cs covering FR-061FR-064 and SC-008.

Checkpoint: Elsa remains the sole issuer of authoritative permission claims, with explicit external mappings and visible provenance.


Phase 6: User Story 4 - Use the Same Broker from Studio Server and WebAssembly (Priority: P1)

Goal: Provide one accessible login chooser and management module across Studio Server and WebAssembly while honoring each host's trust boundary.

Independent Test: Run the same configured connection from both Studio hosts; verify server-side confidential exchange/cookies, WebAssembly public PKCE exchange/memory-only tokens, exact origins, chooser fallback, and opt-in persistence warnings.

Tests for User Story 4

  • T058 [P] [US4] Add shared chooser component tests for deterministic ordering, local/external methods, automatic-default escape, error fallback, unavailable state, and trusted icons in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Login/LoginChooserTests.cs covering FR-066FR-071 and FR-084/SC-015.
  • T059 [P] [US4] Add Blazor Server integration tests for confidential host exchange, HTTP-only session, server-held refresh, return paths, and logout in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/BlazorServer/ServerBrokerAuthenticationTests.cs covering FR-074 and FR-077FR-078.
  • T060 [P] [US4] Add WebAssembly Playwright tests for mandatory PKCE, exact origin, memory-only default, optional session/durable warnings, reload/tab behavior, refresh rotation, and logout in /Users/sipke/Projects/Elsa/elsa-studio/tests/browser/ExternalAuthentication/broker-authentication.spec.ts covering FR-075FR-078 and SC-001/SC-011.

Implementation for User Story 4

  • T061 [P] [US4] Implement typed discovery/exchange/logout clients and authentication state abstractions in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Client/ and Services/.
  • T062 [US4] Implement the accessible Login Method chooser, default redirect loop guard, explicit chooser escape, and safe unavailable/error states in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Pages/Login.razor covering FR-066FR-071 and FR-084.
  • T063 [P] [US4] Implement Blazor Server challenge/callback/logout controllers and confidential code exchange in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorServer/Controllers/ExternalAuthenticationController.cs covering FR-034FR-035 and FR-074/FR-077FR-078.
  • T064 [US4] Implement secure HTTP-only Studio Server sessions, server-side refresh storage, and Elsa API authorization in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorServer/Services/ServerExternalAuthenticationStateProvider.cs covering FR-043FR-048 and FR-074.
  • T065 [P] [US4] Implement WebAssembly PKCE/state generation, callback exchange, in-memory default token accessor, and rotating refresh in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorWasm/Services/ covering FR-035 and FR-075FR-078.
  • T066 [US4] Implement explicit tab-session and durable browser storage options with security warnings in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.BlazorWasm/Extensions/ServiceCollectionExtensions.cs covering FR-076.
  • T067 [US4] Register the shared, Server, and WebAssembly features in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/ExternalAuthenticationFeature.cs and the host-specific feature classes covering FR-074FR-076 and FR-107.
  • T068 [US4] Integrate the deployment-selected broker mode with /Users/sipke/Projects/Elsa/elsa-studio/src/hosts/Elsa.Studio.Host.Server/Program.cs and /Users/sipke/Projects/Elsa/elsa-studio/src/hosts/Elsa.Studio.Host.Wasm/Program.cs without enabling it by default covering FR-104FR-107.

Checkpoint: Both Studio hosts use the same broker contract with host-appropriate credential handling.


Phase 7: User Story 5 - Operate Connections Safely (Priority: P2)

Goal: Give administrators safe testing, Preview Sign-in, session revocation, lockout recovery, and redacted operational signals without adding continuous health or audit storage.

Independent Test: Test and preview a draft, verify stale observations after material changes, prove Preview creates no account/session/token, disable the final normal method only through the guarded override path, and revoke an external session.

Tests for User Story 5

  • T069 [P] [US5] Add test/observation and stale-revision contract tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Operations/ConnectionTestTests.cs covering FR-013, FR-015, FR-089FR-091.
  • T070 [P] [US5] Add Preview Sign-in isolation, authorization, one-time result, redaction, and no-side-effect tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Operations/PreviewSignInTests.cs covering FR-086FR-088 and SC-010.
  • T071 [P] [US5] Add outbound HTTP egress, SSRF, DNS rebinding, timeout, redirect, size, proxy, and exception conformance tests in test/unit/Elsa.ExternalAuthentication.UnitTests/Security/OutboundProviderHttpTests.cs covering FR-092FR-099 and SC-011.
  • T072 [P] [US5] Add notification exhaustiveness and redaction tests in test/unit/Elsa.ExternalAuthentication.UnitTests/Notifications/SecurityNotificationTests.cs covering FR-096, FR-100FR-101, and SC-004/SC-014.
  • T073 [P] [US5] Add final-login-path guard, Break-glass invisibility, session revocation, and connection-disable tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Operations/RecoveryAndRevocationTests.cs covering FR-046, FR-102FR-103, and SC-007.

Implementation for User Story 5

  • T074 [P] [US5] Implement on-demand connection testing and shared latest redacted observation endpoints in src/modules/Elsa.ExternalAuthentication/Endpoints/Connections/TestConnection.cs and Services/ConnectionTestService.cs covering FR-013, FR-015, FR-089FR-090.
  • T075 [P] [US5] Implement the separately tagged opt-in ASP.NET Core health-check bridge in src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationHealthCheck.cs covering FR-091.
  • T076 [US5] Implement administrator-bound preview initiation/callback/result endpoints and one-time stores in src/modules/Elsa.ExternalAuthentication/Endpoints/Previews/ covering FR-086FR-088.
  • T077 [P] [US5] Implement hardened outbound provider HTTP handling and configurable secure egress policies in src/modules/Elsa.ExternalAuthentication/Services/ProviderHttpClientFactory.cs and Validation/OutboundDestinationValidator.cs covering FR-092FR-099.
  • T078 [P] [US5] Implement external-session listing and revocation endpoints in src/modules/Elsa.ExternalAuthentication/Endpoints/Sessions/ covering FR-042FR-046, FR-085, and FR-100.
  • T079 [US5] Implement the final-login-path guard, privileged confirmation, and Break-glass reachability contract in src/modules/Elsa.ExternalAuthentication/Services/FinalLoginPathGuard.cs covering FR-102FR-103.
  • T080 [US5] Add Studio Test, Preview Sign-in, stale observation, session list/revoke, lockout warning, and recovery UI in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Components/Operations/ and Pages/Sessions/Index.razor covering FR-083, FR-086FR-090, and FR-102FR-103.
  • T081 [US5] Publish redacted typed notifications from every sign-in outcome and privileged connection, policy, secret, test, preview, link, and session operation through src/modules/Elsa.ExternalAuthentication/Services/ExternalAuthenticationSecurityNotifier.cs covering FR-096 and FR-100FR-101.

Checkpoint: Operational testing, recovery, preview, revocation, and notification flows are safe and observable without health history or a built-in audit store.


Phase 8: User Story 6 - Extend Providers and Policies (Priority: P2)

Goal: Prove deployed adapters, policies, grant sources, descriptor-driven forms, custom editors, and settings migration can extend the feature without changing the connection schema.

Independent Test: Install a conformance adapter with unique versioned settings and a custom policy, configure both through the generic UI, migrate an old settings version, and authenticate without broker schema changes.

Tests for User Story 6

  • T082 [P] [US6] Add adapter/policy/grant-source registry conformance tests with duplicate IDs, deployment allowlists, version migration, descriptor completeness, and unsupported adapters in test/unit/Elsa.ExternalAuthentication.UnitTests/Extensibility/ExtensionConformanceTests.cs covering FR-017FR-025 and SC-003.
  • T083 [P] [US6] Add generic descriptor-editor and optional custom-editor component tests in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Extensibility/DescriptorEditorTests.cs covering FR-018FR-021 and SC-003.

Implementation for User Story 6

  • T084 [P] [US6] Implement immutable installed adapter, policy, and grant-source registries with stable identifiers and deployment allowlists in src/modules/Elsa.ExternalAuthentication/Services/ExtensionRegistries.cs covering FR-017.
  • T085 [P] [US6] Implement descriptor schema validation, conditional visibility, capability metadata, UI hints, secret metadata, and custom-editor contract versions in src/modules/Elsa.ExternalAuthentication/Services/ExtensionDescriptorValidator.cs covering FR-018FR-019.
  • T086 [US6] Implement adapter-owned opaque settings compatibility and migration orchestration in src/modules/Elsa.ExternalAuthentication/Services/AdapterSettingsMigrationService.cs covering FR-020FR-021.
  • T087 [P] [US6] Add a test-only conformance adapter and custom policy/grant source in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Fixtures/ConformanceExtensions/ covering FR-025 and SC-003.
  • T088 [US6] Implement the generic Studio descriptor form renderer with complete fallback controls in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Components/Descriptors/DescriptorForm.razor covering FR-018FR-021.
  • T089 [US6] Implement the versioned optional custom-editor registry and safe fallback in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Services/CustomConnectionEditorRegistry.cs covering FR-019.

Checkpoint: New trusted provider and policy extensions require deployed code but no broker schema or generic Studio changes.


Phase 9: User Story 7 - Administer External Identity Links (Priority: P2)

Goal: Let authorized administrators inspect, prelink, and unlink external identities with strict tenant and uniqueness enforcement.

Independent Test: Prelink an external tuple to a tenant user, authenticate into that user, reject a cross-tenant target and concurrent duplicate link, then unlink and observe the configured unlinked policy on the next sign-in.

Tests for User Story 7

  • T090 [P] [US7] Add link list/prelink/unlink, tenant isolation, uniqueness convergence, archived-connection retention, and policy fallback integration tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Links/ExternalIdentityLinkTests.cs covering FR-011 and FR-049FR-059 and SC-005.
  • T091 [P] [US7] Add tenant-scoped minimal user lookup authorization and data-minimization tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Links/UserLookupTests.cs covering FR-056, FR-081FR-082.
  • T092 [P] [US7] Add Studio link administration component tests in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Links/ExternalIdentityLinksTests.cs covering FR-059, FR-080FR-085.

Implementation for User Story 7

  • T093 [P] [US7] Implement tenant-scoped paginated external-link list and detail endpoints in src/modules/Elsa.ExternalAuthentication/Endpoints/IdentityLinks/GetIdentityLinks.cs covering FR-050, FR-056, FR-059, and FR-082.
  • T094 [P] [US7] Implement permission-guarded minimal tenant user lookup in src/modules/Elsa.ExternalAuthentication/Endpoints/IdentityLinks/FindUsers.cs covering FR-056 and FR-081FR-082.
  • T095 [US7] Implement transactional prelink and unlink endpoints with tuple uniqueness, tenant matching, archived identity retention, and notifications in src/modules/Elsa.ExternalAuthentication/Endpoints/IdentityLinks/ covering FR-011, FR-050FR-051, FR-055FR-059, FR-085, and FR-100.
  • T096 [P] [US7] Add external-link resources and minimal user lookup to src/clients/Elsa.Api.Client/Resources/ExternalAuthentication/IdentityLinks/.
  • T097 [US7] Implement Studio External Identity Links list, filters, prelink user picker, tuple display, and unlink confirmation in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Pages/IdentityLinks/ covering FR-059 and FR-080FR-085.
  • T098 [US7] Integrate link-management permission visibility with the Studio Security menu in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/Menu/ExternalAuthenticationMenu.cs covering FR-082FR-085.

Checkpoint: Link administration is explicit, tenant-safe, concurrency-safe, and independent of mutable profile attributes.


Phase 10: User Story 8 - Migrate Existing Direct OpenID Connect Deployments (Priority: P3)

Goal: Preserve direct Studio OpenID Connect while making broker mode an explicit, validated alternative with actionable migration guidance.

Independent Test: Start each Studio host in Direct, Brokered, and invalid mixed modes; prove Direct behavior is unchanged, Brokered behavior works, and mixed configuration fails startup with remediation guidance.

Tests for User Story 8

  • T099 [P] [US8] Add Studio startup matrix tests for Direct, Brokered, local-only, and ambiguous modes in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication.Tests/Compatibility/AuthenticationModeTests.cs covering FR-104FR-107 and SC-013.
  • T100 [P] [US8] Add Core regression tests for existing /identity/login and /identity/refresh-token contracts in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Compatibility/LegacyIdentityEndpointTests.cs covering FR-045 and FR-073.

Implementation for User Story 8

  • T101 [US8] Add explicit authentication-mode options and fail-fast mutual-exclusion validation in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.Authentication.Abstractions/ covering FR-104FR-105.
  • T102 [US8] Preserve existing Direct OpenID Connect registrations and select Brokered mode only when configured in /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.Authentication.OpenIdConnect.BlazorServer/Extensions/ServiceCollectionExtensions.cs and Elsa.Studio.Authentication.OpenIdConnect.BlazorWasm/Extensions/ServiceCollectionExtensions.cs covering FR-104FR-105.
  • T103 [P] [US8] Document direct-to-broker setting mappings, unchanged secret ownership, explicit mode switch, rollback, and both host variants in doc/migrations/external-authentication.md and /Users/sipke/Projects/Elsa/elsa-studio/docs/migrations/external-authentication.md covering FR-104FR-107.
  • T104 [US8] Add configuration-owned migration examples for Server and WebAssembly to specs/012-external-authentication/quickstart.md covering FR-106FR-108.
  • T105 [US8] Verify and document unchanged direct-login behavior and additive broker-local behavior in src/modules/Elsa.Identity/README.md and /Users/sipke/Projects/Elsa/elsa-studio/src/modules/Elsa.Studio.ExternalAuthentication/README.md covering FR-073 and FR-104FR-108.

Checkpoint: Existing deployments remain stable until an explicit, validated migration.


Phase 11: Polish and Cross-Cutting Verification

Purpose: Validate the combined delivery against its security, accessibility, compatibility, scale, and documentation gates.

  • T106 [P] Add cross-surface leakage contract tests for responses, redirects, logs, notifications, tests, previews, health details, and Studio models in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Security/SensitiveDataLeakageTests.cs covering FR-028, FR-033, FR-065, FR-096FR-098, and SC-004.
  • T107 [P] Add multi-node initiation/callback/exchange/refresh and cross-node mutation consistency tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Distributed/MultiNodeBrokerTests.cs covering FR-038FR-046, FR-090, and SC-006/SC-007.
  • T108 [P] Add tenant discovery/link fuzz tests and 10,000-connection registry paging tests in test/integration/Elsa.ExternalAuthentication.IntegrationTests/Scale/TenantAndRegistryScaleTests.cs covering FR-009FR-016, FR-050, and SC-005.
  • T109 [P] Add component accessibility tests and Playwright axe/keyboard/screen-reader-semantics/trusted-asset checks in /Users/sipke/Projects/Elsa/elsa-studio/tests/browser/ExternalAuthentication/accessibility.spec.ts covering FR-067FR-069, FR-083FR-084, and SC-015.
  • T110 [P] Add benchmark/load scenarios for discovery, management paging, and broker overhead in test/performance/Elsa.Workflows.PerformanceTests/ExternalAuthentication/ExternalAuthenticationBenchmarks.cs covering the performance goals in plan.md.
  • T111 Review all External Authentication XML docs, README files, option defaults, permission descriptions, endpoint summaries, and configuration examples in src/modules/Elsa.ExternalAuthentication/, src/modules/Elsa.ExternalAuthentication.OpenIdConnect/, src/modules/Elsa.ExternalAuthentication.Secrets/, and specs/012-external-authentication/quickstart.md.
  • T112 Run dotnet test test/unit/Elsa.ExternalAuthentication.UnitTests/Elsa.ExternalAuthentication.UnitTests.csproj and dotnet test test/unit/Elsa.Identity.UnitTests/Elsa.Identity.UnitTests.csproj from the elsa-core repository.
  • T113 Run dotnet test test/integration/Elsa.ExternalAuthentication.IntegrationTests/Elsa.ExternalAuthentication.IntegrationTests.csproj from the elsa-core repository.
  • T114 Run dotnet build Elsa.sln from the elsa-core repository and resolve new warnings or errors without unrelated cleanup.
  • T115 Run dotnet test src/modules/Elsa.Studio.ExternalAuthentication.Tests/Elsa.Studio.ExternalAuthentication.Tests.csproj and the External Authentication Playwright suite from /Users/sipke/Projects/Elsa/elsa-studio/.
  • T116 Run dotnet build Elsa.Studio.sln from /Users/sipke/Projects/Elsa/elsa-studio/ and validate both quickstart host configurations.

Phase 12: Approved Revision Follow-up (Implemented; Full-Solution Verification Open)

Goal: Reconcile the completed baseline with the approved host-wide environment, record-ID/logical-key, full-shadow, user-matching, static create-user role, and Authentication.UI design.

Core model, persistence, and registry

  • T117 [P] Add immutable logical Connection Key to durable External Identity Links and long-lived External Authentication Sessions, including their filters/DTOs and link tuple uniqueness, while retaining stable record IDs in connection management routes and broker transaction/preview/observation records; add migration and boundary tests covering FR-008, FR-037, FR-050, FR-055, FR-086, and SC-005SC-007.
  • T118 Implement explicit complete Studio Override creation and registry resolution, including no field merge, disabled-continues-shadowing, archived-reveals-configuration, and restore-resumes-shadowing; add source/lifecycle/concurrency tests covering FR-006FR-012 and SC-002/SC-012.
  • T119 [P] Make SSO administration host-wide within the currently connected Elsa server environment, remove/reject any invented DeploymentTarget/ServerEnvironment entity or editable field, and preserve existing Elsa user/link target-tenant isolation tests covering FR-009, FR-016, FR-037, and SC-005.

OIDC and secret ownership

  • T120 Implement and migrate OpenID Connect settings v2 with exact HTTPS discoveryUrl, deployment-derived immutable-Connection-Key callback, confidential upstream client, mandatory S256 PKCE, exact serialized client_secret_basic/client_secret_post, and no useUserInfo; add permission/deployment-gated Advanced issuer/authorization/token/signing-key overrides with confirmation, persistent warning, notification, descriptor/migration/request/callback tests, and conformance tests proving validation invariants cannot be disabled, covering FR-024 and FR-029FR-031.
  • T121 [P] Implement the built-in configuration-key ISecretBindingResolver, add Managed/External ownership to bindings and management DTOs, restrict replace/remove to Managed Secrets, derive safe generation fingerprints for External Secrets, and test no-reveal/rotation behavior covering FR-026FR-028 and FR-098.

Admission, roles, and sessions

  • T122 Implement per-connection policy selection and the generic matcher-based policy with one IExternalUserMatcher, descriptor-declared ephemeral required claims, single-match linking, Reject/CreateUser no-match fallback, ambiguous/error rejection, and no first-party verified-email matcher; implement static defaultRoleIds authorization in the newly created User write and add policy/privilege/concurrency tests covering FR-057FR-064 and SC-008SC-010.
  • T123 [P] Enforce Elsa-initiated login/logout only and minimal upstream token retention: discard upstream access/refresh tokens after callback/user-info, retain only protected adapter logout material when required, and purge it by external-session end; add leakage/lifecycle tests covering FR-048AFR-048B, FR-065, FR-096, and SC-004.
  • T124 Update REST/runtime/client contracts for record-ID management/transient records, Connection Key links/sessions, implicit host-wide environment, overrides, preferred state, user matcher descriptors/policy preview, static create-user roles, and Managed/External Secret state; remove v1 claim-role/permission mapping endpoints and add compatibility tests.

Studio composition and management

  • T125 [P] Create the Studio Settings navigation/composition foundation without a Settings backend domain; contribute one-level SSO at /settings/sso-connections (legacy aliases may remain) and test duplicate-key/order/authorization behavior covering FR-080 and FR-084A.
  • T126 [P] Create Elsa.Studio.Authentication.UI as the generic login/logout shell using ILoginMethodCatalog, ILoginMethodComponentProvider, and ILoginMethodIconProvider; move generic chooser/accessibility/return-path behavior into the shell and test local/external composition covering FR-066FR-078 and FR-084A.
  • T127 Update the External Authentication Studio contribution and editor for record ID plus immutable key, implicit host environment, exact discovery URL, read-only derived callback, basic/post client authentication, permission-gated Advanced trust overrides with warning/confirmation, explicit full-shadow overrides, Managed/External Secrets, matcher-based policy, static create-user defaultRoleIds, test, and Preview covering FR-004FR-031 and FR-080FR-088.
  • T128 [P] Keep External Identity Links and External Authentication Sessions as separate capability/permission-gated Security pages, use Connection Key in their DTOs/routes, and add navigation/authorization tests covering FR-050, FR-059, FR-080FR-085.
  • T129 Remove or hide claim/group-to-permission/role, wildcard, and pass-through mapping UI from this release; replace Preview permission projection with user-match/no-match and static create-user-role projection and add absence/regression tests covering FR-060FR-065.
  • T130 Update chooser discovery/rendering so one method may be preferred for deterministic ordering/emphasis but never automatically redirects; remove redirect-loop/escape behavior and add preferred-unavailable/accessibility tests covering FR-067FR-071 and SC-015.

Compatibility, documentation, and verification

  • T131 Preserve Direct OIDC and broker mode as installed-compatible but route-owner-exclusive Studio modes; add startup matrix, parity, migration, warning, and rollback tests/documentation for staged deprecation with no removal before a future major release covering FR-104FR-109 and SC-013.
  • T132 Generate/update all supported EF Identity migrations and snapshots for durable link/session Connection Key, retained record IDs, override provenance, secret ownership, matcher-policy/static create-user roles, and minimal logout material; document migration and rollback.
  • T133 Run targeted Core unit/integration suites for T117T124, including configuration-first and all supported persistence providers.
  • T134 Run Studio component and browser suites for T125T131 in both Server and WebAssembly hosts.
  • T135 Run dotnet build Elsa.sln, dotnet build Elsa.Studio.sln, cross-node broker tests, sensitive-data leakage tests, and the quickstart scenario; only then mark the approved revision implemented.
  • T136 Add an extensible Identity Role-deletion dependency coordinator and External Authentication contributor; enumerate all database/configuration CreateUser and matcher no-match defaultRoleIds references across lifecycle states, return sanitized configuration paths, block ordinary deletion, and implement authorized dependency-version/revision-prevalidated atomic-or-safe-best-effort editable-reference remediation with empty-default-role confirmation, partial-progress retry diagnostics, REST/runtime contract tests, and no new Studio page, covering FR-063AFR-063F and SC-016.

Dependencies and Execution Order

Phase Dependencies

  • Setup (Phase 1) has no dependencies.
  • Foundational (Phase 2) depends on Setup and blocks all user stories.
  • US1 (Phase 3) is the configuration-first MVP.
  • US2 (Phase 4) depends on the foundational source/store contracts; it can proceed alongside US1 after those contracts stabilize.
  • US3 (Phase 5) depends on token and session contracts from Foundation and integrates with US1 at T054.
  • US4 (Phase 6) depends on the public broker contract from US1 but its shared UI and host-state services can begin against the frozen REST contract.
  • US5 (Phase 7) depends on Foundation; preview callback integration depends on the adapter path from US1.
  • US6 (Phase 8) depends only on the foundational extension contracts and can proceed in parallel with US1US5.
  • US7 (Phase 9) depends on the atomic link contract from Foundation and integrates with the resolver from US1.
  • US8 (Phase 10) depends on the Studio broker mode from US4.
  • Polish (Phase 11) depends on every selected story.
  • Approved Revision Follow-up (Phase 12) depends on the completed baseline; T117T124 and T125T131 can proceed by repository/contracts in parallel, while T132T135 are integration gates.

User Story Completion Order

Setup → Foundation ┬→ US1 ─┬→ US3
                   │       ├→ US4 → US8
                   │       ├→ US5
                   │       └→ US7
                   ├→ US2
                   └→ US6

US1US8 → Polish and Cross-Cutting Verification

Within Each User Story

  • Write the listed tests first and confirm that they fail for the intended missing behavior.
  • Implement models and infrastructure before services, services before endpoints/components, and endpoints before end-to-end verification.
  • Complete the independent test before treating a story as done.

Parallel Execution Examples

  • US1: T024T027 can run together; T028 and T030 can run together before T029/T031T035.
  • US2: T036T038 can run together; T041, T043T045 can run in parallel after T039.
  • US3: T048T050 can run together; T051 and T053 can run together before T052/T054.
  • US4: T058T060 can run together; T061, T063, and T065 can run together against the REST contract.
  • US5: T069T073 can run together; T074, T075, T077, and T078 can run together.
  • US6: T082T083 can run together; T084, T085, and T087 can run together.
  • US7: T090T092 can run together; T093, T094, and T096 can run together.
  • US8: T099T100 and T103 can run together before final mode integration.

Requirements Coverage

Requirement range Primary tasks
FR-001FR-016 T010, T016T017, T036T042, T108
FR-017FR-031 T011, T015, T024, T028T029, T043T047, T082T089
FR-032FR-048 T010, T018T019, T025T026, T032T035, T050, T054, T059T065, T073, T078, T107
FR-049FR-065 T011, T027, T030T031, T048T057, T090T098
FR-066FR-084 T012, T023, T025T026, T032, T058T068, T091T092, T097T098, T109
FR-085FR-103 T012T014, T023, T026, T038, T042, T047, T069T081, T106
FR-104FR-108 T068, T099T105
SC-001SC-003 T024T025, T036T038, T082T089
SC-004SC-007 T026, T037, T072T073, T090, T106T108
SC-008SC-011 T027, T048T057, T060, T070T073
SC-012SC-015 T036T037, T072, T081, T099T105, T109

Implementation Strategy

MVP First

  1. Complete Setup and Foundation.
  2. Complete US1 with configuration-owned OpenID Connect and in-memory single-node stores.
  3. Demonstrate discovery, external callback, JIT/link resolution, permission issuance, code exchange, refresh, and logout independently.

Incremental Delivery

  1. Add US2 for persisted management and cross-node state.
  2. Add US3 and US4 for full Elsa authorization and both Studio hosts.
  3. Add US5US7 for operational safety, extension conformance, and link administration.
  4. Add US8 compatibility guidance and finish the cross-cutting gates.

Notes

  • [P] means different files and no dependency on an incomplete task in the same phase.
  • Configuration-first/single-node deployments may use in-memory state; multi-node deployments require EF state plus shared Data Protection.
  • No task adds a continuous health monitor, health history, audit database, social-provider shortcuts, end-user self-linking, or a general OAuth authorization server.
  • Commit after each coherent implementation slice rather than mechanically after every checkbox.