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

# OAuth Device Flow

> How a CLI authenticates against the management API: request a code, send the user to the Portal, poll for a token

The **device flow** lets a command-line tool authenticate a human without handling their credentials and without running a local redirect server. The tool shows a code, the human approves it in a browser, the tool polls until a token appears.

<Note>
  This flow lives on the **platform**, at `api.noorle.com`, and the human-facing approval page is on the **Portal**. It is not served by the identity boundary at `auth.noorle.com` — the boundary advertises only the authorization-code grant. The two issue different tokens signed with different key material.
</Note>

## The shape

```mermaid theme={null}
graph TD
    A["CLI: POST /oauth/device/authorize"]
    B["Response: device_code, user_code,<br/>verification URI, expires_in, interval"]
    C["CLI prints the URL and the code"]
    D["Human opens the Portal page,<br/>enters the code, approves"]
    E["CLI polls POST /oauth/token"]
    F["Token"]

    A --> B
    B --> C
    C --> D
    C --> E
    E -->|authorization_pending| E
    D --> F
    E --> F
```

## Step 1 — request a device code

```bash theme={null}
POST https://api.noorle.com/oauth/device/authorize
{ "client_id": "your-client-id" }
```

The response carries:

| Field                       | Value                                                                       |
| --------------------------- | --------------------------------------------------------------------------- |
| `device_code`               | `dc_` followed by 32 random alphanumerics. The secret half — keep it local. |
| `user_code`                 | Eight characters formatted `XXXX-XXXX`. What the human types.               |
| `verification_uri`          | The Portal's device-verify page                                             |
| `verification_uri_complete` | The same URL with the code pre-filled                                       |
| `expires_in`                | **900** — fifteen minutes                                                   |
| `interval`                  | **5** — poll no faster than every five seconds                              |

The user-code alphabet excludes visually ambiguous characters, so a code read aloud or off a screen is unambiguous.

## Step 2 — the human approves

Send them to `verification_uri_complete` if you can; otherwise print the plain URL and the code separately, because a code that has to be retyped is a code that gets typo'd.

```
To authorize, open:

  <verification_uri from the response>

and enter the code:

  ABCD-2345

Waiting…
```

Print the `verification_uri` the response gave you rather than a URL you hard-coded. The Portal host it points at is environment-specific, and the response is the authority on it.

## Step 3 — poll for the token

```bash theme={null}
POST https://api.noorle.com/oauth/token
{
  "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
  "code": "dc_…"
}
```

<Warning>
  The device code goes in the **`code`** field, not a `device_code` field. That is the shape this endpoint accepts.
</Warning>

While waiting, the endpoint returns `authorization_pending`. Other outcomes: `slow_down` if you are polling too fast, `access_denied` if the human declined, and an expired-token error past fifteen minutes.

On success:

```json theme={null}
{
  "access_token": "eyJ…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJ…",
  "scope": "account:manage:{account_id}"
}
```

The session is cleaned up on success, so a device code is genuinely single-use.

## What you get back

**Access token.** A JWT signed HS256, valid **one hour**. Send it as `Authorization: Bearer …`.

**Scope.** `account:manage:{account_id}` — the scope identifies which account the token is good for. There is no read/write/admin scope split; authority comes from the approving user's role and grants.

<Note>
  Plan for the hour. When an access token expires, run the device flow again — the human approves once more and you get a fresh token.
</Note>

## Using the token

```bash theme={null}
curl https://api.noorle.com/v1/agents \
  -H "Authorization: Bearer $TOKEN"
```

The token's issuer is stamped with the management-API host that served it, and validation checks that issuer. A token minted against one environment does not validate against another.

## Storing it

The token is a bearer credential for one hour. Treat it accordingly:

* A file with owner-only permissions, or the OS keychain.
* Never a world-readable location, never a shell history, never a log line.
* In a container, pass it as an environment variable rather than baking it into an image.

## Security notes

**The device code is the secret.** Anyone holding it can poll for the token. It never goes on screen, never in a URL you display, never in a log.

**The user code is not a secret**, but a wrong code is a code entered against someone else's pending request. Fifteen minutes of validity bounds the exposure.

**Both codes are single-use.** A successful exchange deletes the session.

## Device flow or API key?

Use the device flow when a person is present and their identity should be on the record — the token carries their principal, so the audit trail names them.

Use an [API key](/docs/learn/auth/api-keys) when nobody is present. The device flow needs a human at a browser by construction, which an unattended job does not have.

## Troubleshooting

| Symptom                         | Cause                                                                                 |
| ------------------------------- | ------------------------------------------------------------------------------------- |
| `authorization_pending` forever | The human never approved. Confirm they opened the page and entered the code.          |
| `slow_down`                     | Polling faster than the returned `interval` of 5 seconds.                             |
| Expired after 15 minutes        | The window elapsed. Start a new device authorization.                                 |
| `access_denied`                 | The human declined.                                                                   |
| `invalid_request`               | Check that the device code is in the `code` field and the grant type string is exact. |
| 401 after an hour               | The access token expired. Start a new device authorization.                           |

***

Next: [JWT Tokens](/docs/learn/auth/jwt-tokens) — what is actually inside these tokens, and which issuer signed them.
