Skip to content

Commit ba26ac5

Browse files
committed
Merge branch 'main' into codex/clarify-team-ownership-model
2 parents 5125f1a + fee97de commit ba26ac5

52 files changed

Lines changed: 1977 additions & 355 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zitadel/components": patch
3+
---
4+
5+
Tidy the web components package: align README/AGENTS docs with the real SDK-config API, adopt idiomatic Lit patterns (`classMap`, `live()`, `ifDefined`, `@query`, a shared `emit()` helper), make post-step focus deterministic via `updateComplete` instead of `requestAnimationFrame`, centralise SDK/API resolution in a `resolveApi()` helper, correct the manifest registry (e.g. `zl-passkey` `method` attribute), and expand unit/browser test coverage.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# 01 — Password Login
2+
3+
The minimal login flow. Collect an identifier, then a password, then complete.
4+
5+
## Capabilities exercised
6+
7+
- Single-purpose definition (`login` only — no flip-table coverage required).
8+
- Identifier-shaped field (`email` is `x-unique` in the user schema) drives
9+
implicit identifier dispatch on submit.
10+
- Password-shaped field (`password` is `x-password`) drives implicit password
11+
verification on submit.
12+
- Terminal `complete: show` — the frontend renders a success screen and the
13+
flow handoff token is returned alongside the terminal step.
14+
15+
## Graph
16+
17+
```mermaid
18+
flowchart TD
19+
start([Start: purpose=login]) --> identifier
20+
identifier["identifier<br/>field: email<br/>action: submit"]
21+
password["password<br/>field: password<br/>action: submit"]
22+
done([done<br/>complete: show])
23+
24+
identifier -- submit --> password
25+
password -- submit --> done
26+
```
27+
28+
## Walk-through
29+
30+
1. `POST /flow` with `purpose: login` → engine resolves this definition, mints
31+
an auth attempt, and returns the `identifier` step.
32+
2. The user submits `email`. The engine resolves the identifier against the
33+
user schema's `x-unique` index. If found, it transitions on `submit` to
34+
`password`. If not found, the engine emits `user_not_found`; this step does
35+
not wire that outcome, so the submit fails with an error (the step
36+
re-renders with `Error` set).
37+
3. The user submits `password`. The engine verifies against the stored
38+
credential. On success, it transitions to `done` and issues a handoff
39+
token on the terminal `FlowStepResult`.
40+
41+
## Notes
42+
43+
- No `user_not_found` route means unknown identifiers fail loudly rather than
44+
routing to a register branch. That is the engine's documented behavior for
45+
solo-purpose login flows. To route unknown users into a registration
46+
sub-flow, add `register` as a purpose and wire `user_not_found` — see
47+
example 05.
48+
- The identifier and password steps are separate, so submitted fields are
49+
validated in two rounds. Putting both fields on a single step is allowed
50+
but only useful for `create_user` registration (example 02).
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"project_id": "${PROJECT_ID}",
3+
"schema_uri": "https://nextgen.com/flow-definition.json",
4+
"flow_definition": {
5+
"name": "password-login",
6+
"user_schema": "${USER_SCHEMA_URL}",
7+
"purposes": {
8+
"login": "identifier"
9+
},
10+
"steps": [
11+
{
12+
"name": "identifier",
13+
"fields": ["email"],
14+
"actions": {
15+
"submit": { "primary": true }
16+
},
17+
"transitions": {
18+
"submit": { "target": "password" }
19+
}
20+
},
21+
{
22+
"name": "password",
23+
"fields": ["password"],
24+
"actions": {
25+
"submit": { "primary": true }
26+
},
27+
"transitions": {
28+
"submit": { "target": "done" }
29+
}
30+
},
31+
{
32+
"name": "done",
33+
"complete": "show"
34+
}
35+
]
36+
}
37+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# 02 — Password Register
2+
3+
Single-step password signup with `on_success: create_user`. The user submits
4+
identifier and password together; the engine creates the user row and the
5+
credential atomically, marks the new user as verified on the auth attempt,
6+
then completes.
7+
8+
## Capabilities exercised
9+
10+
- Single-purpose definition (`register` only).
11+
- `on_success: create_user` writer — hashes the password (argon2id), creates
12+
the user, persists the password credential, then registers the new user on
13+
the auth attempt so the terminal handoff issues.
14+
- Writer-manifest validation: `create_user`'s manifest is
15+
`[identifier, password]`; both kinds are collected on this step, so the
16+
validator accepts the definition.
17+
- Terminal `complete: show`.
18+
19+
## Graph
20+
21+
```mermaid
22+
flowchart TD
23+
start([Start: purpose=register]) --> signup
24+
signup["signup<br/>fields: email, password<br/>on_success: create_user"]
25+
done([done<br/>complete: show])
26+
27+
signup -- submit --> done
28+
```
29+
30+
## Walk-through
31+
32+
1. `POST /flow` with `purpose: register` → engine returns the `signup` step.
33+
2. The user submits `email` and `password`. Field validation runs against the
34+
user schema (email format, password min length, uniqueness lookup).
35+
3. `on_success: create_user` fires before the transition: user row + password
36+
credential are written in one transaction; the auth attempt is marked as
37+
having a registered user.
38+
4. The `submit` transition routes to `done`. The terminal step issues the
39+
handoff token.
40+
41+
## Notes
42+
43+
- Multi-step password signup (collect identifier on one step, password on the
44+
next, then `on_success: create_user`) is supported through the
45+
ancestor-chain manifest pattern — the validator walks reverse adjacency
46+
from the `on_success` step and accepts identifier collection on any
47+
upstream step. Example 05 uses this pattern.
48+
- Adding a `user_already_exists` transition is optional here. Without it, a
49+
duplicate email errors loudly on `email`'s uniqueness check.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"project_id": "${PROJECT_ID}",
3+
"schema_uri": "https://nextgen.com/flow-definition.json",
4+
"flow_definition": {
5+
"name": "password-register",
6+
"user_schema": "${USER_SCHEMA_URL}",
7+
"purposes": {
8+
"register": "signup"
9+
},
10+
"steps": [
11+
{
12+
"name": "signup",
13+
"fields": ["email", "password"],
14+
"actions": {
15+
"submit": { "primary": true }
16+
},
17+
"on_success": "create_user",
18+
"transitions": {
19+
"submit": { "target": "done" }
20+
}
21+
},
22+
{
23+
"name": "done",
24+
"complete": "show"
25+
}
26+
]
27+
}
28+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# 03 — Passkey Login (Discoverable)
2+
3+
A single-step passkey login. The user is identified entirely through the
4+
WebAuthn assertion — no identifier is collected up front, so the engine issues
5+
the assertion challenge with an empty `allowCredentials` list and the
6+
authenticator picks a credential it holds for the relying party.
7+
8+
## Capabilities exercised
9+
10+
- Action-driven dispatch: the `passkey` action is engine-handled. Selecting
11+
it triggers the two-phase ceremony rather than a field-shaped submit.
12+
- **Phase 1 (issue)** — the step re-renders with a `challenge` payload
13+
(`method`, `challenge_id`, `options`) and the state cookie rotates.
14+
- **Phase 2 (verify)** — the user submits the assertion `proof`. The engine
15+
verifies via `auth-attempt.SubmitPasskey`, resolves the user from the
16+
credential, marks the attempt verified, then routes through the `passkey`
17+
transition.
18+
- Terminal `complete: show`.
19+
20+
## Graph
21+
22+
```mermaid
23+
flowchart TD
24+
start([Start: purpose=login]) --> signin
25+
signin["signin<br/>action: passkey<br/>no fields"]
26+
issue{{"phase 1: issue<br/>(challenge emitted)"}}
27+
verify{{"phase 2: verify<br/>(proof submitted)"}}
28+
done([done<br/>complete: show])
29+
30+
signin -- "passkey action" --> issue
31+
issue -- "challenge_response.proof" --> verify
32+
verify -- "passkey (after verify)" --> done
33+
```
34+
35+
## Walk-through
36+
37+
1. `POST /flow` with `purpose: login` → engine returns the `signin` step with
38+
the `passkey` action.
39+
2. The user picks `passkey`. The engine calls
40+
`auth-attempt.IssuePasskeyChallenge`; the step re-renders with the
41+
assertion options. `allowCredentials` is empty because no user is
42+
identified — discoverable credentials only.
43+
3. The browser produces an assertion. The frontend re-submits with
44+
`challenge_response.{challenge_id, proof, method}`. The engine verifies,
45+
resolves the user, and transitions on `passkey` to `done`.
46+
4. The terminal step issues the handoff token.
47+
48+
## Notes
49+
50+
- To support **non-discoverable** credentials (passkeys not stored in a
51+
resident key), add an identifier field on a prior step so the engine can
52+
populate `allowCredentials` with the resolved user's credential IDs before
53+
issuing the assertion. Example 06 illustrates this pattern (identifier step
54+
exposes both `submit` and `passkey` actions).
55+
- RPID is derived per-request from `WithRequestHostMiddleware` so same-origin
56+
fetches without an `Origin` header still work.
57+
- The state cookie rotates on every step transition, including the issue and
58+
verify legs of the ceremony.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"project_id": "${PROJECT_ID}",
3+
"schema_uri": "https://nextgen.com/flow-definition.json",
4+
"flow_definition": {
5+
"name": "passkey-login",
6+
"user_schema": "${USER_SCHEMA_URL}",
7+
"purposes": {
8+
"login": "signin"
9+
},
10+
"steps": [
11+
{
12+
"name": "signin",
13+
"actions": {
14+
"passkey": { "primary": true }
15+
},
16+
"transitions": {
17+
"passkey": { "target": "done" }
18+
}
19+
},
20+
{
21+
"name": "done",
22+
"complete": "show"
23+
}
24+
]
25+
}
26+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# 04 — Passkey Register
2+
3+
Identifier-first passkey signup. The user submits an email, then the WebAuthn
4+
ceremony runs; the engine creates the user row in the same transaction that
5+
persists the credential.
6+
7+
## Capabilities exercised
8+
9+
- Action-driven dispatch via the engine-handled `passkey_register` action.
10+
- Field validation runs on the submitted `email` (format + uniqueness)
11+
before the ceremony begins; a duplicate fails loudly on the field rather
12+
than after challenge issue.
13+
- **Provisional user pattern**:
14+
- On the issue leg, the engine calls `GenerateUserID()` and stashes the
15+
result in `CollectedData["_user_id"]`, plus a `_passkey_provisional`
16+
marker so the verify leg knows to finalize the user.
17+
- On the verify leg, `HandleProvisional` writes the user row (with `email`
18+
as the identifier attribute) **inside the same DB transaction** that
19+
persists the credential. If verification fails, neither row is written.
20+
- After verify, `RegisterCreatedUser` marks the user as verified on the
21+
auth attempt so the terminal handoff issues.
22+
- No `on_success: create_user` — its writer manifest is
23+
`[identifier, password]`, and this flow collects no password. The
24+
provisional pattern handles user creation instead.
25+
26+
## Graph
27+
28+
```mermaid
29+
flowchart TD
30+
start([Start: purpose=register]) --> signup
31+
signup["signup<br/>field: email<br/>action: passkey_register"]
32+
issue{{"phase 1: issue<br/>mint provisional _user_id<br/>(challenge emitted)"}}
33+
verify{{"phase 2: verify<br/>HandleProvisional creates user<br/>RegisterCreatedUser"}}
34+
done([done<br/>complete: show])
35+
36+
signup -- "passkey_register action" --> issue
37+
issue -- "challenge_response.proof" --> verify
38+
verify -- "passkey_register (after verify)" --> done
39+
```
40+
41+
## Walk-through
42+
43+
1. `POST /flow` with `purpose: register` → engine returns the `signup` step
44+
with the `email` field and `passkey_register` action.
45+
2. The user enters `email` and picks `passkey_register`. The engine validates
46+
`email` against the schema, merges it into collected data, mints a
47+
provisional `_user_id`, calls
48+
`passkey-registration.IssuePasskeyRegistrationChallenge`, and re-renders
49+
the step with the creation `options`.
50+
3. The browser produces an attestation. The frontend re-submits with
51+
`challenge_response.{challenge_id, proof, method}`.
52+
4. The engine sees the `_passkey_provisional` marker and calls
53+
`HandleProvisional` to write the user row (with `email` as the identifier
54+
attribute), then `SubmitPasskeyRegistration` to persist the credential —
55+
all within one transaction. Finally `RegisterCreatedUser` is called against
56+
the auth attempt.
57+
5. The `passkey_register` transition routes to `done`; the terminal step
58+
issues the handoff token.
59+
60+
## Notes
61+
62+
- The `passkey_register` action short-circuits the field-shaped identifier
63+
dispatch — `user_not_found` is not emitted on submit, so a register-only
64+
flow does not need to wire it. Field validation (format, uniqueness) still
65+
runs on the email before the ceremony begins.
66+
- The WebAuthn `user.id` is the provisional `_user_id`, kept stable across
67+
phase 1 and phase 2.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"project_id": "${PROJECT_ID}",
3+
"schema_uri": "https://nextgen.com/flow-definition.json",
4+
"flow_definition": {
5+
"name": "passkey-register",
6+
"user_schema": "${USER_SCHEMA_URL}",
7+
"purposes": {
8+
"register": "signup"
9+
},
10+
"steps": [
11+
{
12+
"name": "signup",
13+
"fields": ["email"],
14+
"actions": {
15+
"passkey_register": { "primary": true }
16+
},
17+
"transitions": {
18+
"passkey_register": { "target": "done" }
19+
}
20+
},
21+
{
22+
"name": "done",
23+
"complete": "show"
24+
}
25+
]
26+
}
27+
}

0 commit comments

Comments
 (0)