Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions docs/src/content/docs/development/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,15 @@ For the complete, generated schema, use `GET /openapi.json`.

### OAuth + CLI Login

- `POST /auth/cli/start`
- `GET /auth/cli/callback`
- `POST /auth/cli/complete`
- `GET /auth/cli/mfa`
- `POST /auth/cli/mfa`
CLI login uses the RFC 8252 loopback flow (see [OAuth Flow](/security/authentication/oauth-flow/)):

- `POST /auth/cli/start` - S256 code challenge, CLI state and loopback `redirect_uri`; returns `auth_url`
- `GET /auth/cli/callback` - identity provider redirect; exchanges the code, then binds the browser
- `GET /auth/cli/mfa` - continues the bound browser to MFA (or enrollment)
- `POST /auth/cli/mfa` - submits an MFA code from the bound browser
- `GET /auth/cli/return` - issues the single-use login code and redirects the browser to the CLI's loopback listener
- `POST /auth/cli/cancel` - cancels the login from the bound browser; returns the loopback URL that tells the CLI
- `POST /auth/cli/complete` - redeems the login code with the CLI's code verifier for a session token
- `GET /auth/web/login` (also supports `HEAD`)
- `GET /auth/web/callback`
- `GET /auth/web/logout`
Expand Down
13 changes: 10 additions & 3 deletions docs/src/content/docs/operations/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ convox logs -a rack-gateway --since 10m

### CLI Login Hangs

The CLI does not run a local callback server. It opens the browser and polls the gateway.
The CLI listens on `http://127.0.0.1:<random port>/callback` and waits (up to 10 minutes) for the browser
to be redirected there with a single-use login code.

<Steps>

Expand All @@ -58,13 +59,19 @@ The CLI does not run a local callback server. It opens the browser and polls the
curl https://gateway.example.com/api/v1/health
```

2. **Use `--no-open` to capture the auth URL**
2. **Use a browser on the same machine as the CLI**

The login code is delivered to `127.0.0.1`. For a remote host, forward the port from the CLI's
"Waiting for the browser to return to `http://127.0.0.1:<port>/callback`" line with
`ssh -L <port>:127.0.0.1:<port> <host>`. Local firewalls must allow connections to `127.0.0.1`.

3. **Use `--no-open` to capture the auth URL**

```bash
rack-gateway login production https://gateway.example.com --no-open
```

3. **Complete the browser flow**
4. **Complete the browser flow**

Ensure the browser can reach the gateway and Google OAuth endpoints.

Expand Down
15 changes: 9 additions & 6 deletions docs/src/content/docs/security/authentication/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,17 @@ The gateway supports two authentication channels:

### CLI Authentication

1. User runs `rack-gateway login`
2. CLI generates PKCE code verifier and challenge
3. Browser opens to Google OAuth with challenge
4. After approval, Google redirects back to the gateway callback
5. CLI polls the gateway to complete login
6. Gateway exchanges code + verifier for ID token and issues a session
1. User runs `rack-gateway login`; the CLI listens on `http://127.0.0.1:<random port>/callback`
2. CLI generates its PKCE code verifier and sends only the S256 challenge, its state and the loopback
redirect URI to the gateway
3. Browser opens to Google OAuth (the URL is also printed in the terminal)
4. The gateway exchanges Google's code at the callback, then binds the login to that browser
5. After MFA, the browser is redirected to the CLI's loopback listener with a single-use login code
6. The CLI redeems the login code with its code verifier and receives a session token
7. Session token stored in `~/.config/rack-gateway/config.json`

See [OAuth Flow](/security/authentication/oauth-flow/) for details.

## Session Management

Sessions are the primary authentication mechanism after initial OAuth:
Expand Down
80 changes: 53 additions & 27 deletions docs/src/content/docs/security/authentication/oauth-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,11 @@ sequenceDiagram
| `prompt` | `select_account` | Always show account picker |
| `hd` | Allowed domain | Filter to organization accounts |

## CLI OAuth Flow (with PKCE)
## CLI Login Flow (RFC 8252 loopback, with PKCE)

The CLI uses OAuth 2.0 with PKCE (Proof Key for Code Exchange) for enhanced security:
The CLI logs in with the [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) loopback flow. Two PKCE pairs
are involved: the gateway's own pair for the Google leg, and the CLI's pair, which proves that only the
CLI that started the login can finish it.

```mermaid
sequenceDiagram
Expand All @@ -94,43 +96,67 @@ sequenceDiagram
participant Browser
participant Google

CLI->>Gateway: POST /api/v1/auth/cli/start
Gateway->>Gateway: Generate state + PKCE
Gateway-->>CLI: auth_url, state, code_verifier
CLI->>CLI: Listen on http://127.0.0.1:<random port>/callback
CLI->>Gateway: POST /api/v1/auth/cli/start (S256 challenge, CLI state, loopback redirect_uri)
Gateway-->>CLI: auth_url (only)

CLI->>Browser: Open auth_url
Browser->>Google: Authorization request + code_challenge
Note over Browser,Google: User authenticates
CLI->>Browser: Open auth_url (also printed in the terminal)
Browser->>Google: Authorization request (gateway PKCE)
Note over Browser,Google: User signs in
Google-->>Browser: Redirect to callback
Browser->>Gateway: GET /api/v1/auth/cli/callback?code=...&state=...
Gateway-->>Browser: Redirect to MFA/success page

CLI->>Gateway: POST /api/v1/auth/cli/complete
Note right of CLI: Includes state + code_verifier (polled)
Gateway->>Gateway: Validate state + MFA
Gateway->>Google: Exchange code + verifier
Gateway->>Google: Exchange code + gateway verifier
Google-->>Gateway: ID token
Gateway->>Gateway: Verify + create session
Gateway-->>Browser: Bind this browser (HttpOnly cookie), continue to MFA

Browser->>Gateway: MFA challenge (or first-factor enrollment)
Browser->>Gateway: GET /api/v1/auth/cli/return
Gateway-->>Browser: Redirect to 127.0.0.1 loopback with single-use login code + CLI state
Browser->>CLI: GET /callback?code=...&state=...

CLI->>Gateway: POST /api/v1/auth/cli/complete (login_code + CLI code_verifier)
Gateway-->>CLI: Session token
```

### Why PKCE?
### What protects the login

- **Loopback redirect only.** `/auth/cli/start` only accepts a redirect URI of the form
`http://127.0.0.1:<port>/callback` (or `http://[::1]:<port>/callback`). The login code is only ever sent to
that address, so a login link someone else started delivers its code to *your* machine, never theirs.
- **Exchange before binding.** The gateway exchanges Google's authorization code at the callback, and only
when that succeeds binds the login to the browser with an HttpOnly, SameSite=Lax cookie (path
`/api/v1/auth/cli`). Every later browser step (MFA form, MFA submit, return, cancel) requires that cookie.
The Google code is never stored.
- **Single-use login code.** The login code is 256 bits of randomness, stored only as a SHA-256 hash, valid
for 2 minutes, issued once per login, and deleted when redeemed (a failed redemption also burns it).
- **CLI PKCE.** Redeeming the login code also requires the CLI's code verifier, which never leaves the CLI.
- **Time limit.** The whole login must finish within 10 minutes.
- **First-factor enrollment.** A user with no MFA factor can enroll one during the login, but only in the
browser bound to the login.
- **Visible initiator.** The MFA page shows the device name and IP that started the login, and the user gets
a "New CLI Login" email when a CLI session is created. **Cancel Login** ends the login on the gateway and
tells the waiting CLI.

### PKCE parameters (CLI pair)

PKCE prevents authorization code interception attacks:
| Parameter | Description |
|-----------|-------------|
| `code_verifier` | 64 random bytes, base64url encoded (86 characters); kept by the CLI |
| `code_challenge` | SHA-256(code_verifier), base64url encoded (43 characters) |
| `code_challenge_method` | Always `S256` |

1. **Code verifier**: High-entropy random string (128 bytes)
2. **Code challenge**: SHA-256 hash of verifier
3. **Verification**: Google verifies the verifier matches the challenge
### Same-machine requirement

Even if an attacker intercepts the authorization code, they cannot exchange it without the code verifier.
The browser must run on the same machine as the CLI, because the login code is delivered to
`127.0.0.1`. To log in from a remote host (for example over SSH), forward the CLI's port: start
`rack-gateway login --no-open`, note the port in the printed "Waiting for the browser to return to
http://127.0.0.1:<port>/callback" line, and run `ssh -L <port>:127.0.0.1:<port> <host>` from the machine with
the browser before opening the printed login URL there.

### PKCE Parameters
### Upgrading

| Parameter | Description |
|-----------|-------------|
| `code_verifier` | 128-byte random string (base64url encoded) |
| `code_challenge` | SHA-256(code_verifier), base64url encoded |
| `code_challenge_method` | Always `S256` |
The gateway and CLI must both support the loopback flow. After upgrading the gateway, install the matching
rack-gateway CLI: an older CLI is told to upgrade, and a newer CLI refuses to log in to an older gateway.

## Token Verification

Expand Down
50 changes: 38 additions & 12 deletions docs/src/content/docs/user-guide/cli/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,27 @@ sequenceDiagram
participant Gateway
participant Google

CLI->>Gateway: 1. Start login
CLI->>Gateway: 1. Start login (loopback redirect URI + PKCE challenge)
Gateway-->>CLI: 2. OAuth URL
CLI->>Google: 3. Open browser
Google-->>Gateway: 4. Auth callback
Gateway-->>CLI: 5. Session token
Google-->>Gateway: 4. Auth callback (gateway exchanges the code)
Gateway-->>CLI: 5. Browser redirected to 127.0.0.1 with a single-use login code
CLI->>Gateway: 6. Redeem login code + PKCE verifier
Gateway-->>CLI: 7. Session token
```

The CLI:
1. Requests a login URL from the gateway
2. Opens your browser to Google's OAuth consent page
3. The gateway receives the OAuth callback and stores the auth code
4. The CLI polls the gateway to complete login
1. Listens on `http://127.0.0.1:<random port>/callback` and starts the login with the gateway
2. Prints the login URL and opens your browser to Google's sign-in page
3. After you sign in (and complete MFA, or enroll a first factor), the browser is sent back to the CLI's
local listener with a single-use login code
4. Redeems the code together with its PKCE verifier for a session token
5. Stores the session token in your config file

The whole login must finish within 10 minutes. The browser must be on the **same machine** as the CLI,
because the login code is delivered to `127.0.0.1` (see [Logging in from a remote host](#logging-in-from-a-remote-host)).
**Cancel Login** on the approval page ends the login and the CLI exits.

## Session Storage

Session tokens are stored in `~/.config/rack-gateway/config.json`:
Expand Down Expand Up @@ -190,17 +197,36 @@ See [MFA Verification](/user-guide/cli/mfa-verification/) for details.
The CLI tries to open your default browser. If it fails:

```bash
# Copy the printed URL and open it manually
# The CLI always prints the login URL; open it manually in a browser on the same machine
rack-gateway login production https://gateway.example.com
# Output: Open this URL in your browser: https://gateway.example.com/api/v1/auth/cli/start?...
# Output: Open this URL in your browser to log in (on this machine): https://accounts.google.com/...
```

### "OAuth callback failed"

The gateway receives the OAuth callback, then the CLI polls until completion. Issues can include:
After Google, the gateway sends the browser to the CLI's local listener on `127.0.0.1`. Issues can include:
- **Gateway not reachable**: DNS/VPN/Tailscale issues
- **Browser blocked the redirect**: allow the callback URL
- **Stale login state**: retry `rack-gateway login`
- **Browser on another machine**: the redirect to `127.0.0.1` can't reach the CLI (see below)
- **Local firewall or security software** blocking connections to `127.0.0.1`
- **Login took longer than 10 minutes**: retry `rack-gateway login`

### Logging in from a remote host

When the CLI runs on a remote machine (for example over SSH), forward its listener port to the machine with
the browser. Run `rack-gateway login --no-open` on the remote host, note the port in the printed
"Waiting for the browser to return to `http://127.0.0.1:<port>/callback`" line, then from the machine with
the browser run:

```bash
ssh -L <port>:127.0.0.1:<port> <remote-host>
```

and open the printed URL in the browser there.

### "Gateway is older than this CLI" / "CLI is too old"

The gateway and CLI must both support the loopback login. Upgrade whichever side is older; after upgrading
the gateway, install the matching rack-gateway CLI.

### "Domain not allowed"

Expand Down
3 changes: 2 additions & 1 deletion docs/src/content/docs/user-guide/cli/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ With Gateway: rack-gateway → Gateway (OAuth session) → Rack API
```

The `rack-gateway` CLI:
- Handles OAuth authentication flow (opens browser, gateway receives callback, CLI polls for completion)
- Handles OAuth authentication flow (opens the browser, receives a single-use login code on a local
`127.0.0.1` listener, and redeems it with its PKCE verifier)
- Stores session tokens securely per-rack
- Wraps Convox commands
- Manages MFA verification when required
Expand Down
20 changes: 13 additions & 7 deletions internal/cli/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,21 @@ The integration tests create backups of the real Convox CLI configuration to pre

### OAuth Flow

The CLI uses PKCE (Proof Key for Code Exchange) for secure OAuth without client secrets:

1. Generate code verifier and challenge
2. Open browser to gateway OAuth endpoint
3. User authenticates with Google
4. Gateway validates and returns authorization code
5. CLI exchanges code for session token
`rack-gateway login` uses the RFC 8252 loopback flow (`cli_login.go`, `login_loopback.go`):

1. Generate a PKCE verifier/challenge and a random state; listen on `http://127.0.0.1:<random port>/callback`
2. `POST /api/v1/auth/cli/start` with the S256 challenge, state and loopback redirect URI; the gateway
returns only `auth_url` (a response with `state`/`code_verifier` means an older gateway → refuse)
3. Always print the login URL, then try to open the browser (only https URLs, or http to a loopback
identity provider when the gateway is loopback)
4. The gateway exchanges Google's code, binds the browser, runs MFA, and redirects the browser to the
loopback listener with a single-use login code (or `error=<code>`, e.g. `cancelled`)
5. Redeem the login code with the verifier at `POST /api/v1/auth/cli/complete` for a session token
6. Token stored in config file

The browser must be on the same machine as the CLI (remote hosts: `ssh -L <port>:127.0.0.1:<port>`).
The login times out after 10 minutes. A gateway upgrade to this flow needs a matching CLI build.

### Error Handling

- Network errors: Suggest checking gateway URL
Expand Down
Loading
Loading