Skip to content

Commit 3c70b84

Browse files
committed
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.
1 parent f869c90 commit 3c70b84

1 file changed

Lines changed: 122 additions & 0 deletions

File tree

src/docs/backend/authentication.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,126 @@ See the [Hooks](/docs/framework/hooks/#onpreuserregister) reference for more det
403403

404404
---
405405

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**.
438+
439+
### Initiate
440+
441+
**Request:**
442+
```http
443+
POST /api/auth/cli/initiate HTTP/1.1
444+
Content-Type: application/json
445+
446+
{
447+
"redirect_uri": "http://127.0.0.1:54321/callback",
448+
"correlation": "a-random-opaque-value",
449+
"code_challenge": "BASE64URL_SHA256_OF_VERIFIER"
450+
}
451+
```
452+
453+
`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.
454+
455+
**Response:**
456+
```json
457+
{
458+
"carrier": "<signed-opaque-carrier>",
459+
"authorize_url": "/dash/login?cli=<signed-opaque-carrier>"
460+
}
461+
```
462+
463+
### Exchange
464+
465+
**Request:**
466+
```http
467+
POST /api/auth/exchange HTTP/1.1
468+
Content-Type: application/json
469+
470+
{
471+
"code": "<one_time_code>",
472+
"code_verifier": "<original_code_verifier>"
473+
}
474+
```
475+
476+
**Response (Success):**
477+
```json
478+
{
479+
"token": "<access_token>",
480+
"expires": "2024-07-01T20:21:10Z",
481+
"refresh_token": "<refresh_token>",
482+
"refresh_token_expires": "2024-07-08T20:21:10Z"
483+
}
484+
```
485+
486+
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. |
501+
502+
Programmatically (Go framework):
503+
504+
```go
505+
app, _ := fastschema.New(&fs.Config{
506+
AuthConfig: &fs.AuthConfig{
507+
CLILogin: &fs.CLILoginConfig{
508+
Enabled: true,
509+
AllowedRedirectHosts: []string{"app.example.com"},
510+
},
511+
},
512+
})
513+
```
514+
515+
### Security notes
516+
517+
- 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+
406526
## Token Usage
407527

408528
Clients must send the access token in every authenticated request. Two methods:
@@ -439,6 +559,8 @@ Cookie: token=<access_token>
439559
| `AUTH_REG_ALLOWED_DOMAINS` | _(empty)_ | Comma-separated allowlist of email domains for signup (see [Registration Policy](#registration-policy)) |
440560
| `AUTH_REG_BLOCKED_DOMAINS` | _(empty)_ | Comma-separated denylist of email domains for signup |
441561
| `AUTH_REG_RESERVED_USERNAMES` | _(empty)_ | Comma-separated list of usernames that cannot be registered |
562+
| `AUTH_CLI_LOGIN_ENABLED` | `false` | Enable CLI / native-app login (see [CLI / Native-App Login](#cli-native-app-login-rfc-8252)) |
563+
| `AUTH_CLI_ALLOWED_REDIRECT_HOSTS` | _(empty)_ | Comma-separated allowlist of extra non-loopback https redirect hosts |
442564

443565
### Email Templates
444566

0 commit comments

Comments
 (0)