You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(auth): document CLI/native-app login and social login flow
Describe the loopback one-time code flow, the /api/auth/exchange and
/api/auth/methods endpoints, the AUTH_CLI_* configuration, and how browser
social login delivers the token through a one-time code instead of a cookie.
Copy file name to clipboardExpand all lines: src/docs/backend/authentication.md
+122Lines changed: 122 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -403,6 +403,126 @@ See the [Hooks](/docs/framework/hooks/#onpreuserregister) reference for more det
403
403
404
404
---
405
405
406
+
## Social / OAuth Login
407
+
408
+
Social providers (GitHub, Google, ...) are configured under `AUTH` (`enabled_providers` + per-provider `client_id`/`client_secret`). The dashboard renders a button per enabled provider; the enabled set is exposed by `GET /api/auth/methods` (`{ "providers": [...], "otp": bool }`, names only, never secrets).
409
+
410
+
Flow (browser):
411
+
412
+
1. The browser navigates to `GET /api/auth/{provider}/login`. The server signs a short-lived `state` carrier, sets a matching state cookie (CSRF binding), and redirects to the provider.
413
+
2. After consent, the provider redirects to `GET /api/auth/{provider}/callback`. The server verifies the `state` (and the binding cookie), then resolves or creates the user.
414
+
3.**The token is not placed in a cookie by the server.** The callback mints a single-use one-time code and redirects the browser to the dashboard login route with the code in the URL fragment (`/dash/login#code=<one_time_code>`).
415
+
4. The dashboard reads the code and calls `auth().exchange(code)` (`POST /api/auth/exchange`), receiving the token over a direct request and persisting it through the SDK auth store. Where the token lives (cookie, localStorage, ...) is the SDK's concern, not the server's.
416
+
417
+
Because the token is delivered through the one-time code rather than a server-set cookie, the storage mechanism stays pluggable on the client. The state cookie set in step 1 is a transient CSRF binding (not the token); it is matched on the callback and lapses by its short TTL.
418
+
419
+
---
420
+
421
+
## CLI / Native-App Login (RFC 8252)
422
+
423
+
A command-line tool or native desktop app can sign a user in **without ever handling the user's password or seeing the JWT in the browser**. The user logs in through the normal dash browser page by **any enabled method** (local, social, or OTP), and the resulting credential is delivered to the app's local loopback listener via a single-use one-time code, following the OAuth 2.0 for Native Apps pattern ([RFC 8252](https://datatracker.ietf.org/doc/html/rfc8252)).
424
+
425
+
This feature is **opt-in and disabled by default**. Enable it with `AUTH_CLI_LOGIN_ENABLED=true`.
426
+
427
+
### Why a one-time code
428
+
429
+
The token is minted server-side and stashed behind a short-lived, single-use one-time code. Only the code travels through the browser redirect; the loopback listener then exchanges the code for the token over a direct server-to-server request. The token itself never appears in any browser URL, response, or cookie during this flow. A PKCE (S256) challenge binds the exchange to the process that started it, defeating code interception.
430
+
431
+
### Flow
432
+
433
+
1. The native app starts a loopback HTTP listener on `127.0.0.1` (any free port) and generates a PKCE `code_verifier`.
434
+
2. App -> `POST /api/auth/cli/initiate` with the loopback `redirect_uri`, an opaque `correlation` value, and `code_challenge = base64url(sha256(code_verifier))`. The server validates the redirect target and returns a signed `carrier` plus an `authorize_url` (`/dash/login?cli=<carrier>`).
435
+
3. The app opens `authorize_url` in the user's browser. The dash renders the enabled login methods and the user signs in by any of them.
436
+
4. On success the server mints the token, stores it under a one-time code, and `302`-redirects the browser to the loopback `redirect_uri?code=<one_time_code>&state=<correlation>`.
437
+
5. The loopback listener -> `POST /api/auth/exchange` with `{ code, code_verifier }`. The server verifies PKCE, atomically consumes the code (single use), and returns the JWT **exactly once**.
`code_challenge` is required (PKCE S256): native-app loopback redirects are interceptable by other local processes, so every code is bound to a verifier only the initiating process holds.
The code is single-use and short-lived (60 seconds); a second exchange, an expired code, or a mismatched `code_verifier` returns `401`.
487
+
488
+
### Redirect target rules
489
+
490
+
`redirect_uri` is strictly validated before any code is issued, so the endpoint can never be used as an open redirector:
491
+
492
+
-**Loopback hosts** (`127.0.0.1`, `::1`, `localhost`) are always allowed on **any port**, over `http` or `https`. This is the normal case for native apps.
493
+
-**Non-loopback hosts** are allowed **only** over `https` and **only** when listed in `AUTH_CLI_ALLOWED_REDIRECT_HOSTS` (exact host match, no wildcards).
494
+
495
+
### Configuration
496
+
497
+
| Variable | Default | Description |
498
+
|---|---|---|
499
+
|`AUTH_CLI_LOGIN_ENABLED`|`false`| Master switch for the feature. When `false`, all `cli` endpoints return `403`. |
500
+
|`AUTH_CLI_ALLOWED_REDIRECT_HOSTS`|_(empty)_| Comma-separated allowlist of non-loopback https redirect hosts. |
- Disabled by default; only loopback redirects are accepted unless you explicitly allowlist https hosts.
518
+
- One-time codes are single-use, short-lived (60s), and high-entropy; PKCE S256 (required) binds the exchange to the initiating process.
519
+
- An exchange attempt consumes the code even when the PKCE verifier is wrong (fail-closed: this prevents brute-forcing the verifier on a live code). If an exchange fails, restart the flow.
520
+
- The token never touches the browser in this flow - it is delivered only over the server-to-server exchange.
521
+
- The browser-mediated step is bound to the browser that started it (a per-session state cookie is matched on the provider callback), closing login CSRF / session fixation.
522
+
- The one-time code store is in-process (single instance). For a multi-node deployment behind a load balancer, the exchange must reach the same instance that minted the code; a shared store is a planned follow-up.
523
+
524
+
---
525
+
406
526
## Token Usage
407
527
408
528
Clients must send the access token in every authenticated request. Two methods:
@@ -439,6 +559,8 @@ Cookie: token=<access_token>
439
559
|`AUTH_REG_ALLOWED_DOMAINS`|_(empty)_| Comma-separated allowlist of email domains for signup (see [Registration Policy](#registration-policy)) |
440
560
|`AUTH_REG_BLOCKED_DOMAINS`|_(empty)_| Comma-separated denylist of email domains for signup |
441
561
|`AUTH_REG_RESERVED_USERNAMES`|_(empty)_| Comma-separated list of usernames that cannot be registered |
0 commit comments