@ory/argus 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ const session = await client.verifySession(sessionToken);
16
16
  const result = await client.checkPermission({
17
17
  namespace: "AgentTools",
18
18
  object: "Bash",
19
- relation: "invoke",
19
+ relation: "use",
20
20
  subjectId: `session:${sessionId}`,
21
21
  });
22
22
 
@@ -57,7 +57,7 @@ async function onBeforeTool(toolName: string, sessionId: string) {
57
57
  const result = await client.checkPermission({
58
58
  namespace: "AgentTools",
59
59
  object: toolName,
60
- relation: "invoke",
60
+ relation: "use",
61
61
  subjectId: `session:${sessionId}`,
62
62
  });
63
63
  if (result.allowed === false) {
@@ -84,7 +84,7 @@ The wrapped Ory client. One instance per harness session.
84
84
  | Sessions and tokens | `verifySession`, `introspectToken`, `classifyError` |
85
85
  | Permission checks | `checkPermission`, `batchCheckPermissions`, `checkMcpPermission` |
86
86
  | Principals (who is acting) | `setUserPrincipal`, `setAgentPrincipal` |
87
- | Delegation tuples | `createRelationship`, `deleteRelationship` |
87
+ | Delegation relations | `createRelationship`, `deleteRelationship` |
88
88
  | Tracing | `tracer` (see Tracer below) |
89
89
 
90
90
  ### Identity gates
@@ -32,6 +32,48 @@ If you find yourself writing `flow.ui.nodes.map(...)` to render auth UI,
32
32
  stop and switch to Ory Elements. Custom node rendering is a fallback,
33
33
  not a parallel option.
34
34
 
35
+ ## Correctness contract — satisfy these or the app will 404 / 500 / CSRF
36
+
37
+ Almost every "Ory doesn't work" report comes from one of four wiring
38
+ mistakes in the generated app — **not** from Ory itself and **not** from
39
+ the agent plugin governing this session. Treat each as a hard requirement,
40
+ and verify all four (Step 10) before telling the user the app is ready.
41
+
42
+ 1. **Ory must be first-party (same site as your app), or every flow fails
43
+ CSRF.** Ory's browser flows depend on a CSRF cookie. If the browser calls
44
+ Ory on a *different* site than your app, the browser drops that cookie and
45
+ you get `CSRF token mismatch`, 403s, or redirect loops. So the browser SDK
46
+ URL must be **your own origin** — never `https://<slug>.projects.oryapis.com`
47
+ directly:
48
+ - **Local dev against Ory Network** — run `ory tunnel` and point the SDK at
49
+ the tunnel (`http://localhost:4000`). See Step 9.
50
+ - **Local dev against the local stack** — the gateway is already on
51
+ `http://localhost:4000` (first-party to `localhost`). Use `http://localhost`,
52
+ not `127.0.0.1`.
53
+ - **Production** — serve Ory from your own domain via an Ory Network custom
54
+ domain (CNAME) or the `@ory/nextjs` proxy, and set the SDK URL to that.
55
+ 2. **The app's routes must match what Ory redirects to, or you get 404s.** Ory
56
+ redirects the browser to the self-service UI URLs configured on the project
57
+ (login, registration, recovery, verification, settings, **error**). If a page
58
+ lives at `/auth/login` but the project points at `/login`, the redirect 404s.
59
+ Pick one set of paths and make these four agree: the page files you create,
60
+ the `@ory/nextjs` UI-path config, the project's `selfservice.flows.*.ui_url`
61
+ (+ `error.ui_url`), and the middleware path lists. Always create an error
62
+ page — Ory redirects there on any flow error. See Step 8.
63
+ 3. **Server-side session/flow calls must forward the request cookies, or they
64
+ 500/401.** A `FrontendApi` built with `credentials: "include"` only sends
65
+ cookies in the **browser**. In Next.js server components, route handlers, and
66
+ middleware you must forward the incoming request's cookies — the `@ory/nextjs`
67
+ server helpers do this for you. Reusing the browser client on the server is
68
+ the single most common 500. See Step 7.
69
+ 4. **The SDK URL env var must be set at runtime, or the SDK 500s on
70
+ construction.** Validate `NEXT_PUBLIC_ORY_SDK_URL` (browser) / `ORY_SDK_URL`
71
+ (server) at startup and fail with a clear message instead of constructing the
72
+ client with `undefined`. See Step 4.
73
+
74
+ If a flow still fails after all four hold, isolate it with the **App bug vs.
75
+ plugin bug** triage in Step 10 before assuming a bug in Ory or in the plugin.
76
+
35
77
  ## Step 1: Check prerequisites
36
78
 
37
79
  Before installing the Ory CLI, decide where the auth backend will run:
@@ -107,29 +149,54 @@ React island or a small client bundle so you still get the Elements UI
107
149
  on the auth routes. Rendering flow nodes directly on the server is the
108
150
  fallback of last resort.
109
151
 
152
+ **Match the installed major versions.** `@ory/nextjs` and
153
+ `@ory/elements-react` change import names and component props across major
154
+ versions. After installing, run `npm ls @ory/nextjs @ory/elements-react`
155
+ and follow the docs and TypeScript types for *that* major — do not assume
156
+ symbol names from memory. A missing import or a prop type error at build
157
+ time is a version mismatch in the generated app, not a plugin bug.
158
+
110
159
  ## Step 4: Configure the Ory SDK
111
160
 
112
- Create a shared Ory client configuration. The SDK URL should come
113
- from an environment variable:
161
+ For **Next.js**, prefer wiring Ory through `@ory/nextjs` (its config proxies
162
+ Ory under your own origin and supplies cookie-forwarding server helpers — this
163
+ satisfies contract #1 and #3 for free). Build a hand-rolled `FrontendApi`
164
+ client only for **browser** code in a React SPA, or for explicit client-side
165
+ calls.
166
+
167
+ When you do build a client, read the URL from an env var and **validate it** —
168
+ constructing the SDK with an `undefined` `basePath` is a 500 waiting to happen
169
+ (contract #4):
114
170
 
115
171
  ```typescript
116
172
  import { Configuration, FrontendApi } from "@ory/client-fetch";
117
173
 
174
+ const sdkUrl = process.env.NEXT_PUBLIC_ORY_SDK_URL ?? process.env.ORY_SDK_URL;
175
+ if (!sdkUrl) {
176
+ throw new Error(
177
+ "Ory SDK URL is not set. Set NEXT_PUBLIC_ORY_SDK_URL (browser) " +
178
+ "or ORY_SDK_URL (server) — see Step 4.",
179
+ );
180
+ }
181
+
182
+ // Browser-only: `credentials: "include"` sends cookies from browser code.
183
+ // Do NOT reuse this client in server components / route handlers / middleware
184
+ // (contract #3) — use the @ory/nextjs server helpers there.
118
185
  const ory = new FrontendApi(
119
- new Configuration({
120
- basePath: process.env.NEXT_PUBLIC_ORY_SDK_URL || process.env.ORY_SDK_URL,
121
- credentials: "include",
122
- })
186
+ new Configuration({ basePath: sdkUrl, credentials: "include" }),
123
187
  );
124
188
 
125
189
  export default ory;
126
190
  ```
127
191
 
128
- Add to the project's `.env` or `.env.local`:
192
+ Add to the project's `.env` or `.env.local`. The **browser** value
193
+ (`NEXT_PUBLIC_ORY_SDK_URL`) must be your own origin (contract #1), so in local
194
+ development point it at the tunnel or the local gateway, not at `*.oryapis.com`:
129
195
 
130
196
  ```bash
131
- NEXT_PUBLIC_ORY_SDK_URL=https://<project-slug>.projects.oryapis.com
132
- # or
197
+ # Local dev (Ory Tunnel for Network, or the local stack gateway):
198
+ NEXT_PUBLIC_ORY_SDK_URL=http://localhost:4000
199
+ # Server-side only (safe to be the direct project URL):
133
200
  ORY_SDK_URL=https://<project-slug>.projects.oryapis.com
134
201
  ```
135
202
 
@@ -166,7 +233,9 @@ Create the following page structure:
166
233
  - `app/auth/registration/page.tsx` — `<Registration flow={flow} />`
167
234
  - `app/auth/recovery/page.tsx` — `<Recovery flow={flow} />`
168
235
  - `app/auth/verification/page.tsx` — `<Verification flow={flow} />`
169
- - `app/auth/settings/page.tsx` — `<Settings flow={flow} />`
236
+ - `app/auth/settings/page.tsx` — `<Settings flow={flow} />`, wrapped in
237
+ `<SessionProvider>` from `@ory/elements-react/client` (Settings is the only
238
+ flow that needs it — see {{REF_LOGIN_FLOW}})
170
239
 
171
240
  Each page initializes its flow with `getLoginFlow`, `getRegistrationFlow`,
172
241
  etc. from `@ory/nextjs/app`, then hands the flow to the matching
@@ -191,17 +260,27 @@ with the user that an Ory Elements island is not viable.
191
260
 
192
261
  ## Step 7: Add session middleware
193
262
 
194
- Protect authenticated routes by checking the session:
263
+ Protect authenticated routes by checking the session. **Where this code runs
264
+ matters** (contract #3):
195
265
 
196
- ```typescript
197
- const session = await ory.toSession();
198
- if (!session) {
199
- // Redirect to login
200
- }
201
- ```
266
+ - **Browser code** can use the `credentials: "include"` client from Step 4
267
+ directly the browser attaches the session cookie:
268
+
269
+ ```typescript
270
+ const session = await ory.toSession(); // browser only
271
+ if (!session) {
272
+ // Redirect to login
273
+ }
274
+ ```
275
+
276
+ - **Server code** (server components, route handlers, middleware) must forward
277
+ the incoming request's cookies. Do **not** reuse the browser client — it has
278
+ no cookies on the server and will throw (→ 500) or always return 401. Use the
279
+ `@ory/nextjs` server helpers, which read and forward the request cookies for
280
+ you.
202
281
 
203
282
  For Next.js, prefer the official `@ory/nextjs` middleware helper since
204
- it pairs with the Elements pages:
283
+ it pairs with the Elements pages and handles server-side cookie forwarding:
205
284
 
206
285
  ```typescript
207
286
  import { createOryMiddleware } from "@ory/nextjs/middleware";
@@ -216,37 +295,118 @@ export const config = {
216
295
  };
217
296
  ```
218
297
 
219
- ## Step 8: Configure allowed redirect URLs
220
-
221
- Update the Ory project to allow redirects back to your app:
222
-
223
- ```bash
224
- ory patch project <project-id> \
225
- --replace '/services/identity/config/selfservice/allowed_return_urls=["http://localhost:3000", "https://your-domain.com"]'
226
- ```
227
-
228
- ## Step 9: Set up the Ory Tunnel for local development
229
-
230
- For local development, use the Ory tunnel to proxy requests and handle cookies:
231
-
232
- ```bash
233
- ory tunnel http://localhost:3000 --project <project-slug>
234
- ```
235
-
236
- This runs a proxy on `http://localhost:4000` that handles cookie domains
237
- correctly for local development. Update your SDK URL to point to the
238
- tunnel during development.
239
-
240
- ## Step 10: Verify the setup
241
-
242
- 1. Start the development server
243
- 2. Navigate to the login page — confirm Ory Elements renders the form,
244
- including any social login buttons configured on the project
245
- 3. Create a test account via the registration page
246
- 4. Verify login works
247
- 5. Test account recovery flow
248
- 6. Test the verification flow
249
- 7. Check that protected routes redirect unauthenticated users
298
+ ## Step 8: Align the project's routes and return URLs with your app
299
+
300
+ This step prevents the 404s in contract #2. Ory redirects the browser to the
301
+ self-service UI URLs it has configured; those must point at pages your app
302
+ actually serves.
303
+
304
+ **Pick one route convention and use it everywhere.** This guide uses
305
+ `/auth/login`, `/auth/registration`, `/auth/recovery`, `/auth/verification`,
306
+ `/auth/settings`, and `/auth/error`. The page files, the middleware path lists,
307
+ and the project config below must all use the same paths.
308
+
309
+ 1. **Create an error page.** Ory redirects to the error UI on *any* flow error;
310
+ if it doesn't exist you get a 404 (or a blank page) instead of a readable
311
+ message. Create `app/auth/error/page.tsx` rendering the Elements error
312
+ component (`<Error>` / the flow-error view for your installed version).
313
+
314
+ 2. **Tell Ory where the pages are.**
315
+ - **Next.js with `@ory/nextjs`:** declare the UI paths in its config (the
316
+ UI-path overrides in `ory.config.ts`) so its helpers, middleware, and proxy
317
+ route to your pages. The SDK serves the UI under your own origin, so you
318
+ usually do not also edit the project's `ui_url`s.
319
+ - **React SPA / no `@ory/nextjs` proxy:** set the project's self-service UI
320
+ URLs to your app's routes:
321
+
322
+ ```bash
323
+ ory patch project <project-id> \
324
+ --replace '/services/identity/config/selfservice/flows/login/ui_url="https://your-domain.com/auth/login"' \
325
+ --replace '/services/identity/config/selfservice/flows/registration/ui_url="https://your-domain.com/auth/registration"' \
326
+ --replace '/services/identity/config/selfservice/flows/recovery/ui_url="https://your-domain.com/auth/recovery"' \
327
+ --replace '/services/identity/config/selfservice/flows/verification/ui_url="https://your-domain.com/auth/verification"' \
328
+ --replace '/services/identity/config/selfservice/flows/settings/ui_url="https://your-domain.com/auth/settings"' \
329
+ --replace '/services/identity/config/selfservice/flows/error/ui_url="https://your-domain.com/auth/error"'
330
+ ```
331
+
332
+ 3. **Allow redirects back to your app** (every origin you run on — dev and prod):
333
+
334
+ ```bash
335
+ ory patch project <project-id> \
336
+ --replace '/services/identity/config/selfservice/allowed_return_urls=["http://localhost:3000", "https://your-domain.com"]'
337
+ ```
338
+
339
+ ## Step 9: Serve Ory first-party (required — contract #1)
340
+
341
+ The browser must reach Ory on the same site as your app, or cookie-based flows
342
+ fail CSRF. How you achieve that depends on the environment:
343
+
344
+ - **Local dev against Ory Network — run the Ory Tunnel.** This is required, not
345
+ optional:
346
+
347
+ ```bash
348
+ ory tunnel http://localhost:3000 --project <project-slug>
349
+ ```
350
+
351
+ It runs a proxy on `http://localhost:4000` that serves Ory first-party to
352
+ `localhost`. Point `NEXT_PUBLIC_ORY_SDK_URL` at the tunnel
353
+ (`http://localhost:4000`) — **not** at `https://<slug>.projects.oryapis.com`.
354
+
355
+ - **Local dev against the local stack** — the gateway is already first-party on
356
+ `http://localhost:4000`; no tunnel needed (switch to {{REF_LOCAL_DEV}}).
357
+
358
+ - **Production** — serve Ory from your own domain via an Ory Network custom
359
+ domain (CNAME) or the `@ory/nextjs` proxy, and set the browser SDK URL to that
360
+ same-origin path.
361
+
362
+ If you skip this step, login will appear to work but `whoami`/`toSession` will
363
+ return 401 and form submits will fail with a CSRF error — the classic symptom of
364
+ a cross-site SDK URL.
365
+
366
+ ## Step 10: Verify the setup — and confirm it's correct, not just present
367
+
368
+ Do not declare success on "the pages render." Run these checks; each maps to a
369
+ contract item so a failure points straight at the cause.
370
+
371
+ 1. **SDK URL is first-party (contract #1).** Confirm the browser SDK URL is your
372
+ own origin (the tunnel/gateway in dev), not `*.oryapis.com`. In the browser
373
+ devtools Network tab, the flow requests should go to your origin and the
374
+ response should `Set-Cookie` a CSRF cookie.
375
+ 2. **The flow API itself works (isolates Ory from your app).** Against the URL
376
+ the browser uses:
377
+
378
+ ```bash
379
+ curl -i "$NEXT_PUBLIC_ORY_SDK_URL/self-service/login/browser"
380
+ ```
381
+
382
+ Expect `200` with a `Set-Cookie: csrf_token...` header. If this fails, the
383
+ problem is Ory config / the tunnel — not your app code.
384
+ 3. **Every redirect target exists (contract #2).** Visit `/auth/login`,
385
+ `/auth/registration`, `/auth/recovery`, `/auth/verification`,
386
+ `/auth/settings`, and `/auth/error` directly. Each must return `200`, not
387
+ `404`. A 404 here means a route/`ui_url` mismatch (Step 8).
388
+ 4. **End-to-end:** register a test account, log in, confirm a protected route
389
+ loads while signed in and redirects to login when signed out, then test
390
+ recovery and verification.
391
+
392
+ ### App bug vs. plugin bug
393
+
394
+ These are two different systems. Keep them straight so issues are filed in the
395
+ right place:
396
+
397
+ - **The generated app** (login pages, CSRF, sessions, the `/auth/*` routes) is
398
+ ordinary code running in the user's project. A `404`/`500`/CSRF on an `/auth/*`
399
+ route, or in the browser Network tab, is an **app/config** issue — work the
400
+ four contract items and the checks above.
401
+ - **The agent plugin** governs *this coding session* — it authenticates the
402
+ agent, checks Ory Permissions before each tool call, and writes trace spans. It
403
+ never serves your app's HTTP routes. Diagnose it with
404
+ `{{NPX}} status` and the debug log (`ORY_AGENT_DEBUG=true`), **not** by looking
405
+ at your app's auth pages.
406
+
407
+ Quick triage: if `curl` to the flow API (check 2) succeeds but the app page
408
+ fails, it's the app. If `curl` fails, it's Ory/tunnel config. Neither is the
409
+ plugin unless `{{NPX}} status` reports a problem.
250
410
 
251
411
  ## Customization
252
412
 
@@ -24,6 +24,27 @@ rendering belongs in the fallback section at the end of this skill.
24
24
  no per-app rewrite needed.
25
25
  - Works in Next.js (App Router and Pages Router) and any React SPA.
26
26
 
27
+ ## Avoid the 404 / 500 / CSRF traps
28
+
29
+ These pages only work if the surrounding wiring is correct — most "the login
30
+ page is broken" reports are wiring, not the pages or the plugin. If you have not
31
+ run {{REF_AUTH_SETUP}}, do that first; it establishes the four invariants these
32
+ pages depend on:
33
+
34
+ - **First-party SDK URL** — the browser must reach Ory on your own origin (the
35
+ Ory Tunnel or local gateway, `http://localhost:4000` in dev), never
36
+ `https://<slug>.projects.oryapis.com` directly, or every form submit fails
37
+ with a CSRF error.
38
+ - **Routes match the project config** — the `/auth/*` paths below must match the
39
+ project's self-service `ui_url`s and the middleware lists, and you must create
40
+ an `/auth/error` page, or Ory's redirects 404.
41
+ - **Server calls forward cookies** — server-side session/flow reads must forward
42
+ the request cookies (the `@ory/nextjs` server helpers do this); reusing the
43
+ browser client on the server 500s.
44
+
45
+ See the **Correctness contract** and the **App bug vs. plugin bug** triage in
46
+ {{REF_AUTH_SETUP}} for the full detail.
47
+
27
48
  ## Before you start
28
49
 
29
50
  Check the project setup:
@@ -243,6 +264,7 @@ Create `app/auth/settings/page.tsx`:
243
264
 
244
265
  ```typescript
245
266
  import { Settings } from "@ory/elements-react/theme"
267
+ import { SessionProvider } from "@ory/elements-react/client"
246
268
  import { getSettingsFlow, OryPageLayout } from "@ory/nextjs/app"
247
269
 
248
270
  export default async function SettingsPage(props: {
@@ -257,31 +279,62 @@ export default async function SettingsPage(props: {
257
279
 
258
280
  return (
259
281
  <OryPageLayout>
260
- <Settings flow={flow} />
282
+ <SessionProvider>
283
+ <Settings flow={flow} />
284
+ </SessionProvider>
261
285
  </OryPageLayout>
262
286
  )
263
287
  }
264
288
  ```
265
289
 
290
+ Unlike the other flows, **`<Settings>` must be wrapped in `<SessionProvider>`**
291
+ from `@ory/elements-react/client`. Settings renders session-dependent controls
292
+ (connected social accounts, unlinking, logout of other sessions) that read from
293
+ the session context; the pre-auth pages (login, registration, recovery,
294
+ verification) have no session yet and do not need the provider.
295
+
266
296
  ### Settings — React SPA
267
297
 
268
298
  Initialize with `createBrowserSettingsFlow` and render
269
- `<Settings flow={flow} />` from `@ory/elements-react/theme`. The
299
+ `<Settings flow={flow} />` from `@ory/elements-react/theme`, wrapped in
300
+ `<SessionProvider>` from `@ory/elements-react/client` (the same requirement
301
+ as the App Router page above — Settings reads session-dependent state). The
270
302
  component handles password changes, profile traits, MFA enrollment,
271
303
  and connected social providers.
272
304
 
305
+ ## Build the error page (do not skip — missing it causes 404s)
306
+
307
+ Ory redirects the browser to the error UI on **any** flow error (expired flow,
308
+ validation failure, misconfiguration). If that route doesn't exist the user
309
+ hits a 404 or a blank page instead of a readable message, and the failure looks
310
+ like a plugin bug when it's just a missing page.
311
+
312
+ Create `app/auth/error/page.tsx` and render the Elements error view for your
313
+ installed version (`getFlowError` from `@ory/nextjs/app` + the Elements error
314
+ component). Make sure its path matches the project's `error.ui_url`
315
+ (see {{REF_AUTH_SETUP}}, Step 8). For a React SPA, fetch the error with
316
+ `getFlowError({ id })` from the `error` query param and display
317
+ `error.error.message`.
318
+
273
319
  ## Add session management
274
320
 
275
- Create a utility to check the current session:
321
+ Create a **browser** utility to check the current session. (For server
322
+ components, route handlers, and middleware, use the `@ory/nextjs` server
323
+ helpers instead — they forward the request cookies. Reusing this browser client
324
+ on the server has no cookies and will 500/401.)
276
325
 
277
326
  ```typescript
278
327
  import { FrontendApi, Configuration, Session } from "@ory/client-fetch";
279
328
 
329
+ const sdkUrl = process.env.NEXT_PUBLIC_ORY_SDK_URL;
330
+ if (!sdkUrl) {
331
+ throw new Error("NEXT_PUBLIC_ORY_SDK_URL is not set — see ory-auth-setup Step 4.");
332
+ }
333
+
334
+ // Browser-only client. `credentials: "include"` only sends cookies in the browser,
335
+ // and sdkUrl must be your own origin (the tunnel/gateway in dev), not *.oryapis.com.
280
336
  const ory = new FrontendApi(
281
- new Configuration({
282
- basePath: process.env.NEXT_PUBLIC_ORY_SDK_URL,
283
- credentials: "include",
284
- })
337
+ new Configuration({ basePath: sdkUrl, credentials: "include" }),
285
338
  );
286
339
 
287
340
  export async function getSession(): Promise<Session | null> {
@@ -343,16 +396,137 @@ For larger design changes, the Ory Elements primitives package exposes
343
396
  the underlying card, button, and input components — use those before
344
397
  falling back to custom node rendering.
345
398
 
399
+ ## Enable MFA and passkeys
400
+
401
+ These are **project-side** capabilities: once enabled on the Ory project, the
402
+ `<Login>`, `<Registration>`, and `<Settings>` components render the second-factor
403
+ and passkey UI automatically — **no page code changes**. Configure them with the
404
+ Ory CLI against your project (`ory list projects` for the id), the same idiom as
405
+ {{REF_SOCIAL_LOGIN}}. Against the local stack, apply the equivalent keys to the
406
+ local Kratos config instead ({{REF_LOCAL_DEV}}).
407
+
408
+ ### Passkeys
409
+
410
+ ```bash
411
+ ory patch identity-config <project-id> \
412
+ --add '/selfservice/methods/passkey/enabled=true' \
413
+ --add '/selfservice/methods/passkey/config/rp/display_name="My App"' \
414
+ --add '/selfservice/methods/passkey/config/rp/id="your-domain.com"' \
415
+ --add '/selfservice/methods/passkey/config/rp/origins=["https://your-domain.com"]'
416
+ ```
417
+
418
+ - `rp.id` is the **domain only** — no scheme, no port (`example.com`, not
419
+ `https://example.com:3000`).
420
+ - `rp.origins` must list the **exact scheme+host+port** the browser uses.
421
+ - For local dev, WebAuthn treats `localhost` as a secure origin: set
422
+ `rp.id="localhost"` and `rp.origins=["http://localhost:3000"]` (match your app
423
+ URL). Passkeys registered against `localhost` won't work on the deployed domain
424
+ and vice-versa.
425
+
426
+ ### MFA methods
427
+
428
+ Enable the second factors you want. Backup codes (`lookup_secret`) should always
429
+ be enabled alongside any other method so a user who loses their device can
430
+ recover.
431
+
432
+ ```bash
433
+ # TOTP (authenticator app)
434
+ ory patch identity-config <project-id> \
435
+ --add '/selfservice/methods/totp/enabled=true' \
436
+ --add '/selfservice/methods/totp/config/issuer="My App"'
437
+
438
+ # Backup codes
439
+ ory patch identity-config <project-id> \
440
+ --add '/selfservice/methods/lookup_secret/enabled=true'
441
+
442
+ # Email code as a second factor
443
+ ory patch identity-config <project-id> \
444
+ --add '/selfservice/methods/code/mfa_enabled=true'
445
+ ```
446
+
447
+ ### Require the second factor (do not skip)
448
+
449
+ Enabling a method only lets users *enroll* — it does not *require* the second
450
+ factor. Without this step MFA is opt-in and unenforced. Set the required
451
+ assurance level to the highest the identity has available:
452
+
453
+ ```bash
454
+ ory patch identity-config <project-id> \
455
+ --add '/selfservice/flows/settings/required_aal="highest_available"' \
456
+ --add '/session/whoami/required_aal="highest_available"'
457
+ ```
458
+
459
+ With `highest_available`, `toSession()` / `getServerSession()` return an
460
+ incomplete session (AAL1) until the user clears the second factor, and Elements
461
+ prompts for it on the next `<Login>`. Your route protection should treat an
462
+ AAL1 session on an MFA-enrolled user as unauthenticated.
463
+
346
464
  ## Test the flow
347
465
 
348
- 1. Start the dev server and Ory tunnel (if developing locally)
349
- 2. Visit `/auth/registration` to create an account — confirm Elements
350
- renders all configured methods (password, social, passkey, etc.)
351
- 3. Visit `/auth/login` to sign in
352
- 4. Verify session is established (check protected routes)
353
- 5. Test `/auth/recovery` with a registered email
354
- 6. Test `/auth/settings` for profile changes
355
- 7. Test logout
466
+ Don't stop at "the page renders." Run these in order each one isolates a
467
+ different failure class:
468
+
469
+ 1. Start the dev server **and the Ory tunnel** (local dev against Network), or
470
+ the local stack. Confirm the browser SDK URL is your own origin, not
471
+ `*.oryapis.com`.
472
+ 2. **Flow API reachable (isolates Ory from your app):**
473
+
474
+ ```bash
475
+ curl -i "$NEXT_PUBLIC_ORY_SDK_URL/self-service/login/browser"
476
+ ```
477
+
478
+ Expect `200` and a `Set-Cookie: csrf_token...` header. If this fails it's the
479
+ tunnel / Ory config, not your pages.
480
+ 3. **No 404s:** visit `/auth/login`, `/auth/registration`, `/auth/recovery`,
481
+ `/auth/verification`, `/auth/settings`, and `/auth/error` — each returns
482
+ `200`. A 404 means a route/`ui_url` mismatch.
483
+ 4. Register at `/auth/registration` — confirm Elements renders all configured
484
+ methods (password, social, passkey, etc.).
485
+ 5. Sign in at `/auth/login`; confirm a protected route loads, and redirects to
486
+ login when signed out.
487
+ 6. Test `/auth/recovery` with a registered email, `/auth/settings` for profile
488
+ changes, and logout.
489
+
490
+ If a submit fails with a CSRF error, the SDK URL is cross-site (trap #1). If a
491
+ redirect 404s, a route doesn't match the project config (trap #2). Neither is a
492
+ plugin bug — see the **App bug vs. plugin bug** triage in {{REF_AUTH_SETUP}}.
493
+
494
+ ### Writing E2E tests (Playwright)
495
+
496
+ Automating these flows has three gotchas that cause flaky or wrong-for-the-wrong-
497
+ reason failures. Get them right up front:
498
+
499
+ 1. **Match URLs by pattern, never literally.** Ory appends `?flow=<uuid>` on
500
+ every flow redirect, so an exact-string wait never matches.
501
+
502
+ ```typescript
503
+ await page.waitForURL(/\/auth\/login\?flow=/) // regex
504
+ await page.waitForURL("**/dashboard**") // glob — also matches query params
505
+ // wrong: await page.waitForURL("http://localhost:3000/auth/login")
506
+ ```
507
+
508
+ 2. **Use strong, dissimilar credentials.** Kratos rejects passwords that are too
509
+ similar to the identifier or that appear in a breach database — a weak fixture
510
+ password fails validation, not the flow you meant to test.
511
+
512
+ ```typescript
513
+ const email = `test-${Date.now()}-${Math.random().toString(36).slice(2)}@example.com`
514
+ const password = "Str0ngP@ssword!123" // fixed, strong, not in any breach list
515
+ ```
516
+
517
+ 3. **Assert on Ory Elements' own testids, not form-level selectors.** Validation
518
+ messages render inside the auth card, keyed by Kratos UI message id (the
519
+ `4xxxxxx` range is validation errors), not on the flow wrapper.
520
+
521
+ ```typescript
522
+ page.locator('[data-testid^="ui/message/4"]') // any validation error
523
+ page.locator('[data-testid="login-auth-card"]').getByText(/credentials|invalid/i)
524
+ // wrong: page.locator('[data-testid="login-flow"]').getByText(...) — errors aren't here
525
+ ```
526
+
527
+ These selectors track the installed `@ory/elements-react` version; if a testid
528
+ assertion breaks after an Elements upgrade, inspect the rendered DOM before
529
+ assuming a flow bug.
356
530
 
357
531
  ## Fallback: rendering UI nodes by hand
358
532
 
@@ -376,8 +550,9 @@ Ory flow API changes manually.
376
550
  - Add social login providers: use {{REF_SOCIAL_LOGIN}}.
377
551
  Elements renders the buttons automatically once providers are
378
552
  configured server-side.
379
- - Add multi-factor authentication via Ory project settings the
380
- `<Login>` and `<Settings>` components handle the second-factor UI.
553
+ - Add multi-factor authentication and passkeys see **Enable MFA and
554
+ passkeys** above. Once the project is configured, the `<Login>` and
555
+ `<Settings>` components render the second-factor and passkey UI automatically.
381
556
  - Customize the identity schema for additional profile fields. Elements
382
557
  reads the schema from the flow and renders new fields automatically.
383
558
  - Set up webhooks for registration events.
@@ -33,6 +33,15 @@ recommend switching to Ory Elements before adding more providers.
33
33
  recommend migrating to Ory Elements so social buttons render for
34
34
  free.
35
35
 
36
+ **Social login depends on the same wiring as the rest of Ory.** Before
37
+ debugging providers, confirm the base app is correct per the Correctness
38
+ contract in {{REF_AUTH_SETUP}}: a first-party SDK URL (the tunnel or local
39
+ gateway in dev, never `*.oryapis.com` in the browser), routes that match the
40
+ project config, and your app origin listed in `allowed_return_urls`. The
41
+ provider redirects the browser back *through Ory* to your app, so a missing
42
+ return URL or a cross-site SDK URL produces a post-login error or 404 that looks
43
+ like a provider bug but isn't.
44
+
36
45
  ## Step 1: Choose providers
37
46
 
38
47
  Ask the user which social login providers they want. Common options: