> ## Documentation Index
> Fetch the complete documentation index at: https://docs.axiomancer.io/llms.txt
> Use this file to discover all available pages before exploring further.

# SSO device flow

> Let employees self-issue short-lived RouteShift API keys by signing in to Google Workspace or Okta from the CLI — no admin has to mint or share credentials.

The SSO device flow lets an employee at a Google Workspace or Okta org run a CLI command, approve it in their browser via a real IdP sign-in, and receive a short-lived `sk-proxy-…` key scoped to their identity. It's a full [RFC 8628](https://datatracker.ietf.org/doc/html/rfc8628) OAuth 2.0 Device Authorization Grant implementation — no admin has to manually mint or share credentials.

## When to use it

Use SSO device-flow keys for:

* **Interactive CLI tools** — anywhere a developer runs a local command that needs a RouteShift key.
* **Notebooks and one-off scripts** — short-lived credentials that auto-expire.
* **Onboarding** — new hires get a working key the moment they can sign in to your IdP, with no admin ticket.

Stick with regular [virtual keys](/routeshift/keys) for:

* **Production services and CI** — long-lived, machine-owned credentials with per-key budgets and model allowlists.
* **Shared agents** — keys that outlive any one employee.

## How the flow works

<Steps>
  <Step title="Employee runs the CLI">
    Their local tool calls `POST /oauth/device/code` with the employee's email. RouteShift looks up the team by email domain (home-realm discovery) and returns a `device_code`, an 8-character `user_code`, and a `verification_uri`.
  </Step>

  <Step title="Employee approves in the browser">
    The CLI prints the code and opens `https://proxy.routeshift.io/oauth/device/verify`. The employee signs in to Google Workspace or Okta, sees the code they were shown, and clicks **Approve**.
  </Step>

  <Step title="RouteShift verifies the identity">
    The IdP redirects back to `/oauth/device/callback` with an authorization code. RouteShift exchanges it for an ID token, validates the issuer, audience, nonce, and email domain, then marks the authorization approved.
  </Step>

  <Step title="CLI receives a key">
    The CLI has been polling `POST /oauth/device/token` with the `device_code`. Once approved, it receives a fresh `sk-proxy-…` token valid for 8 hours.
  </Step>
</Steps>

Keys minted this way are stamped with `metadata.issued_via = "sso_device_flow"` and `metadata.email = "<verified-email>"`. Only one live SSO key is allowed per `(team, email)` pair — a second flow for the same identity replaces the previous key.

## Key properties

| Property       | Value                                                                                                                                     |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **TTL**        | 8 hours (fixed)                                                                                                                           |
| **Scope**      | Full workspace API access — same as any virtual key. Combine with team-level budgets and rate limits for guardrails.                      |
| **Uniqueness** | One live SSO key per `(team, email)`. Re-running the flow revokes the previous key.                                                       |
| **Audit**      | Every issuance writes an `sso_issued` event to the [key audit log](/routeshift/keys#key-lifecycle), including the verified email and IdP. |
| **Revocation** | Manual via **Keys → … → Revoke**, or automatic when the sweeper clears the row after `expires_at`.                                        |

<Note>
  The 8-hour TTL is a deliberate defense against leaked keys — even if the token ends up in a repo or a chat log, its blast radius closes overnight.
</Note>

## Configuring an IdP connection

Every team that wants SSO device flow needs one `idp_config` row per login domain. Provision it via the [admin API](/routeshift/api/admin#sso-device-flow):

```bash theme={null}
curl -X POST https://api.routeshift.io/admin/idp-configs \
  -H "Authorization: Bearer ROUTESHIFT_ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "team_id": "team_…",
    "provider": "okta",
    "login_domain": "acme.com",
    "issuer": "https://acme.okta.com",
    "client_id": "0oab…",
    "client_secret": "…"
  }'
```

Requirements:

* **`provider`** — `google_workspace` or `okta`.
* **`login_domain`** — the email domain employees sign in with (e.g. `acme.com`). A domain belongs to exactly one team; conflicts return `409`.
* **`issuer`** — the OIDC issuer URL. RouteShift fetches its discovery document at create time to validate reachability and the JWKS endpoint. Do not include a trailing slash — the issuer is compared byte-for-byte against the `iss` claim during token verification.
* **`client_id` / `client_secret`** — the OAuth app you registered in the IdP console. The secret is encrypted at rest with `PROVIDER_KEY_SECRET`.
* **Redirect URI** — register `${SSO_CALLBACK_BASE_URL}/oauth/device/callback` in the IdP app console. This must match the environment variable exactly.

<Warning>
  The IdP-side redirect URI must exactly match `SSO_CALLBACK_BASE_URL` configured on the proxy. A mismatch causes the browser leg to fail with an opaque IdP error before RouteShift ever sees the callback.
</Warning>

## CLI example

A minimal client that walks the flow:

```ts theme={null}
async function ssoLogin(email: string): Promise<string> {
  // 1. Request a device code
  const start = await fetch("https://proxy.routeshift.io/oauth/device/code", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email }),
  }).then((r) => r.json());

  console.log(`Open ${start.verification_uri_complete}`);
  console.log(`Or visit ${start.verification_uri} and enter code: ${start.user_code}`);

  // 2. Poll for the key
  while (true) {
    await new Promise((r) => setTimeout(r, start.interval * 1000));
    const res = await fetch("https://proxy.routeshift.io/oauth/device/token", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        device_code: start.device_code,
        grant_type: "urn:ietf:params:oauth:grant-type:device_code",
      }),
    });
    const body = await res.json();
    if (res.ok) return body.access_token; // sk-proxy-…
    if (body.error === "authorization_pending" || body.error === "slow_down") continue;
    throw new Error(body.error_description ?? body.error);
  }
}
```

The device-code endpoint returns:

```json theme={null}
{
  "device_code": "…",
  "user_code": "BCDF-GHJK",
  "verification_uri": "https://proxy.routeshift.io/oauth/device/verify",
  "verification_uri_complete": "https://proxy.routeshift.io/oauth/device/verify?user_code=BCDF-GHJK",
  "expires_in": 600,
  "interval": 5
}
```

The token endpoint returns standard RFC 8628 errors:

| Error                   | Meaning                                                       |
| ----------------------- | ------------------------------------------------------------- |
| `authorization_pending` | Keep polling — the user hasn't approved yet.                  |
| `slow_down`             | You're polling too fast. Increase the interval by 5 seconds.  |
| `expired_token`         | The `device_code` expired (10 minutes). Start over.           |
| `access_denied`         | The user clicked **Deny** on the verify page.                 |
| `invalid_grant`         | The `device_code` is unknown, already consumed, or malformed. |

## Rollout gate

The device flow is disabled by default. To enable it on a deployment:

1. Set `SSO_DEVICE_FLOW_ENABLED=true` on `apps/proxy`.
2. Set `SSO_CALLBACK_BASE_URL` to the public HTTPS origin of the proxy (must match the redirect URI you register in each IdP).
3. Provision at least one `idp_config` via the admin API.

<Warning>
  `SSO_DEVICE_FLOW_ENABLED` is a **per-team onboarding gate**, not a global kill switch. Once a team has an `idp_configs` row, flipping the flag off does not stop that team's employees from logging in. To disable a specific team, delete their `idp_configs` row via `DELETE /admin/idp-configs/{id}?team_id=…`.
</Warning>

## Audit

Every SSO-issued key writes an `sso_issued` event to `api_key_audit_events` with the verified email, IdP config ID, and issuance timestamp. Filter for these in the audit log to see which employees are actively using the flow.

Revocation, rotation, and expiry follow the same [key lifecycle](/routeshift/keys#key-lifecycle) as any other virtual key.
