# OAuth and device login

> How a hosted MCP client connects without a pasted key, and how the CLI signs in with a code.

Two flows need no key to start. The OAuth 2.1 authorization server of `/v1/mcp` lets a hosted client register itself, send the person to the console to approve, and exchange a code (with PKCE) for an access token and a refresh token; the discovery documents are at `/.well-known/oauth-authorization-server` and `/.well-known/oauth-protected-resource/v1/mcp`. The device login is what `ry login` does: it asks for a code, the person confirms it in the console, and the CLI receives a key.

Scopes of the OAuth server: `mcp` and `offline_access`. There are no client secrets; every client is a public client bound to PKCE.

## Endpoints

- [`POST /v1/oauth/register`](#post-v1oauthregister): Registers a public OAuth client (dynamic client registration) and answers its client id. No secret
- [`GET /v1/oauth/authorize`](#get-v1oauthauthorize): Starts an authorization: answers a redirect into the console, where the person approves
- [`POST /v1/oauth/token`](#post-v1oauthtoken): Exchanges an authorization code (with the PKCE verifier) for an access token and a refresh token, or rotates a refresh token
- [`POST /v1/oauth/consent/lookup`](#post-v1oauthconsentlookup): The console reads a pending authorization to show it
- [`POST /v1/oauth/consent/decide`](#post-v1oauthconsentdecide): The console records the person's decision
- [`POST /v1/device/code`](#post-v1devicecode): Starts a device login: answers a device code and a user code to confirm in the console
- [`POST /v1/device/token`](#post-v1devicetoken): The CLI polls here until the login was confirmed, then receives a key

## POST /v1/oauth/register

Registers a public OAuth client (dynamic client registration) and answers its client id. No secret.

**Auth:** No API key: this is an entrance of a login or OAuth flow.

### Request body

JSON, the shape `OAuthClientRegistration` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `redirect_uris` | array of string | required | 1 to 10 items. |
| `client_name` | string | optional | 1 to 120 characters. |
| `token_endpoint_auth_method` | string | optional | at most 40 characters. |
| `grant_types` | array of string | optional | 0 to 4 items. |
| `response_types` | array of string | optional | 0 to 4 items. |
| `scope` | string | optional | at most 200 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/oauth/register" \
  -H "Content-Type: application/json" \
  -d '{"redirect_uris":["<redirect_uri>"]}'
```

### Response

`201` with a JSON object carrying `client_id`, `client_name`, `redirect_uris`, `grant_types`, `response_types`, `token_endpoint_auth_method`, `client_id_issued_at`.

## GET /v1/oauth/authorize

Starts an authorization: answers a redirect into the console, where the person approves.

**Auth:** No API key: this is an entrance of a login or OAuth flow.

### Example request

```bash
curl -X GET "https://api.rytiv.app/v1/oauth/authorize"
```

### Response

`302` with a JSON body.

## POST /v1/oauth/token

Exchanges an authorization code (with the PKCE verifier) for an access token and a refresh token, or rotates a refresh token.

**Auth:** No API key: this is an entrance of a login or OAuth flow.

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/oauth/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=<code>" \
  --data-urlencode "code_verifier=<verifier>" \
  --data-urlencode "client_id=<client id>" \
  --data-urlencode "redirect_uri=<redirect uri>"
```

### Response

A JSON body on success.

## POST /v1/oauth/consent/lookup

The console reads a pending authorization to show it.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `OAuthConsentLookupRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `requestId` | string | required | 1 to 120 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/oauth/consent/lookup" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"requestId":"<requestId>"}'
```

### Response

`200` with a JSON body.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
| 409 | `not-answerable` |  |

## POST /v1/oauth/consent/decide

The console records the person's decision.

**Auth:** API key with full access (a key with scopes cannot call this route). Refused for every agent token (`agent-token-credential`).

### Request body

JSON, the shape `OAuthConsentDecideRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `requestId` | string | required | 1 to 120 characters. |
| `approve` | boolean | required |  |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/oauth/consent/decide" \
  -H "Authorization: Bearer $RYTIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"requestId":"<requestId>","approve":true}'
```

### Response

`200` with a JSON object carrying `redirectUrl`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |
| 401 | `unauthenticated` | No API key, or one that does not work. |
| 403 | `token-scope` | The key's scopes do not cover this route. |
| 404 | `not-found` | Not found, or not yours. An object of another organization answers like one that does not exist. |
| 409 | `not-answerable` |  |

## POST /v1/device/code

Starts a device login: answers a device code and a user code to confirm in the console.

**Auth:** No API key: this is an entrance of a login or OAuth flow.

### Request body

JSON, the shape `DeviceCodeRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `client` | string | optional | 1 to 120 characters. |
| `version` | string | optional | 1 to 40 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/device/code" \
  -H "Content-Type: application/json" \
  -d '{"client":"<client>"}'
```

### Response

`201` with a JSON object carrying `deviceCode`, `userCode`, `verificationUri`, `verificationUriComplete`, `expiresIn`, `interval`.

### Errors

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `bad-request` | The request is malformed or a field is out of range. `error` names every field; `issues` lists them. |

## POST /v1/device/token

The CLI polls here until the login was confirmed, then receives a key.

**Auth:** No API key: this is an entrance of a login or OAuth flow.

### Request body

JSON, the shape `DeviceTokenRequest` of the contract.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `deviceCode` | string | required | 1 to 200 characters. |

### Example request

```bash
curl -X POST "https://api.rytiv.app/v1/device/token" \
  -H "Content-Type: application/json" \
  -d '{"deviceCode":"<deviceCode>"}'
```

### Response

`200` with a JSON object carrying `token`, `org`.
