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

# Device authorization

> OAuth 2.0 device flow for CLIs, smart TVs, and input-constrained devices. RFC 8628.

The device authorization grant ([RFC 8628](https://www.rfc-editor.org/rfc/rfc8628)) lets a device that cannot show a browser, a CLI tool, smart TV, game console, or IoT sensor, authenticate by delegating the sign-in step to a secondary device the user already trusts.

The device displays a short code like `BDFK-RSTV`. The user opens a URL on their phone or laptop, signs in, types the code, and the waiting device gets a session. No credentials ever travel through the constrained device.

## Setup

<Steps>
  <Step>
    ### Add the plugin

    ```typescript title="lib/kavach.ts" theme={"system"}
    import { createKavach } from 'kavachos';
    import { deviceAuth } from 'kavachos/auth'; // [!code highlight]

    const kavach = await createKavach({
      database: { provider: 'postgres', url: process.env.DATABASE_URL! },
      secret: process.env.KAVACH_SECRET!,
      baseUrl: 'https://auth.example.com',
      plugins: [
        deviceAuth({ // [!code highlight]
          verificationUri: 'https://app.example.com/device', // [!code highlight]
        }), // [!code highlight]
      ],
    });
    ```
  </Step>

  <Step>
    ### Build the user-facing approval page

    Create a page at your `verificationUri`. It should let a signed-in user enter the code the device is showing and approve or deny the request.

    ```typescript title="app/device/page.tsx (Next.js)" theme={"system"}
    export default function DevicePage({
      searchParams,
    }: {
      searchParams: { user_code?: string };
    }) {
      // user_code is pre-filled when the device uses verification_uri_complete
      const [code, setCode] = useState(searchParams.user_code ?? '');

      async function approve() {
        await fetch('/auth/device/authorize', {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ user_code: code, action: 'approve' }), // [!code highlight]
        });
      }

      async function deny() {
        await fetch('/auth/device/authorize', {
          method: 'POST',
          credentials: 'include',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ user_code: code, action: 'deny' }), // [!code highlight]
        });
      }

      return (
        <form>
          <input value={code} onChange={e => setCode(e.target.value)} placeholder="XXXX-XXXX" />
          <button type="button" onClick={approve}>Approve</button>
          <button type="button" onClick={deny}>Deny</button>
        </form>
      );
    }
    ```

    The `/auth/device/authorize` endpoint requires an active session. The user must be signed in before they can approve or deny a device code.
  </Step>
</Steps>

## Device flow

<Steps>
  <Step>
    ### Request codes (on the device)

    `POST /auth/device/code`

    The device calls this endpoint to start a new authorization attempt. No authentication required.

    ```typescript title="CLI tool" theme={"system"}
    const res = await fetch('https://auth.example.com/auth/device/code', {
      method: 'POST',
    });

    const data = await res.json();
    // {
    //   device_code: 'a3f9c2e1d8b04f7a...',  // opaque, used for polling
    //   user_code: 'BDFK-RSTV',              // shown to the user
    //   verification_uri: 'https://app.example.com/device',
    //   verification_uri_complete: 'https://app.example.com/device?user_code=BDFK-RSTV',
    //   expires_in: 900,                     // seconds until the code expires
    //   interval: 5,                         // minimum seconds between poll requests
    // }

    console.log(`Open ${data.verification_uri} and enter: ${data.user_code}`);
    ```

    Display `user_code` prominently, this is what the user types. `verification_uri_complete` includes the code as a query parameter, so you can also show a QR code for it.
  </Step>

  <Step>
    ### Poll for authorization (on the device)

    `POST /auth/device/token`

    Poll this endpoint at the `interval` returned in the previous step (default: every 5 seconds). Keep polling until you get an `authorized` response or the code expires.

    ```typescript title="CLI tool" theme={"system"}
    const { device_code, interval, expires_in } = data;
    const deadline = Date.now() + expires_in * 1000;

    while (Date.now() < deadline) {
      await new Promise(resolve => setTimeout(resolve, interval * 1000));

      const pollRes = await fetch('https://auth.example.com/auth/device/token', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ device_code }), // [!code highlight]
      });

      const result = await pollRes.json();

      if (pollRes.ok && result.authorized) {
        console.log('Authorized! User ID:', result.user_id); // [!code highlight]
        break;
      }

      if (result.error === 'slow_down') { // [!code highlight]
        // Server asked for a longer interval
        interval = result.interval; // [!code highlight]
        continue;
      }

      if (result.error === 'access_denied') {
        console.error('User denied the request.');
        break;
      }

      if (result.error === 'expired_token') {
        console.error('Code expired. Start over.');
        break;
      }

      // 'authorization_pending', keep polling
    }
    ```
  </Step>

  <Step>
    ### User approves (on the secondary device)

    The user opens `verification_uri` on their phone or laptop, signs in, and enters the code. The approval page calls `/auth/device/authorize` with the user code and `action: 'approve'`. The next poll from the device returns `{ authorized: true, user_id: '...' }`.
  </Step>
</Steps>

## User code format

User codes use the format `XXXX-XXXX`, two four-character segments separated by a hyphen. The character set is consonants only (`BCDFGHJKLMNPQRSTVWXZ`), which avoids:

* Visually ambiguous characters (no `0`/`O`, `1`/`I`, `5`/`S`)
* Characters that read awkwardly when pronounced aloud

The alphabet and segment length are fixed. The `codeLength` option controls the length of each segment (default: 4).

Input on the approval page is case-insensitive and whitespace-tolerant, `bdfk rstv`, `BDFK-RSTV`, and `BDFKRSTV` all resolve to the same grant.

## Polling interval and slow-down

The initial interval is returned by the `/auth/device/code` endpoint (default: 5 seconds). The server enforces a minimum gap between polls. If the device polls too quickly, the response includes `error: 'slow_down'` and an updated `interval` value. The device must use the new interval for all subsequent polls.

<Warning>
  Polling too frequently will trigger `slow_down` responses. Use the `interval` from the initial response as your starting poll delay, and always update it when you receive `slow_down`.
</Warning>

## Endpoints

| Endpoint                 | Method | Auth required | Description                                                |
| ------------------------ | ------ | ------------- | ---------------------------------------------------------- |
| `/auth/device/code`      | `POST` | No            | Start a new device flow, returns device code and user code |
| `/auth/device/token`     | `POST` | No            | Poll for authorization status                              |
| `/auth/device/authorize` | `POST` | Yes           | User approves or denies a device code                      |

### `/auth/device/code` response

| Field                       | Type     | Description                                                     |
| --------------------------- | -------- | --------------------------------------------------------------- |
| `device_code`               | `string` | Opaque code used for polling. Keep this secret on the device    |
| `user_code`                 | `string` | Short human-readable code shown to the user (e.g. `BDFK-RSTV`)  |
| `verification_uri`          | `string` | URL the user visits to approve                                  |
| `verification_uri_complete` | `string` | Same URL with `user_code` as a query param, useful for QR codes |
| `expires_in`                | `number` | Seconds until the codes expire (default: 900)                   |
| `interval`                  | `number` | Minimum seconds between poll requests (default: 5)              |

### `/auth/device/token` error codes

| `error`                 | Meaning                                                  |
| ----------------------- | -------------------------------------------------------- |
| `authorization_pending` | User has not acted yet, keep polling                     |
| `slow_down`             | Polling too fast, use the new `interval` in the response |
| `access_denied`         | User denied the request, stop polling                    |
| `expired_token`         | Code has expired, start over with a new code             |

## Configuration reference

| Option                | Type     | Default  | Description                                                       |
| --------------------- | -------- | -------- | ----------------------------------------------------------------- |
| `verificationUri`     | `string` | required | URL the user visits to enter the code and approve                 |
| `codeLength`          | `number` | `4`      | Length of each segment in the user code (`4` → `XXXX-XXXX`)       |
| `codeExpirySeconds`   | `number` | `900`    | How long the device code and user code stay valid                 |
| `pollIntervalSeconds` | `number` | `5`      | Minimum interval between polling attempts, returned to the device |
