# OAuth applications

OAuth2 sign-in for a seeded application: consent, the authorization_code, refresh_token and client_credentials grants, 24-hour expiry on the simulator clock, refresh replay for 30 minutes and revocation.

An OAuth2 application signs in to the simulator as it does to Linear, through three routes:

- `GET /oauth/authorize` gives consent.
- `POST /oauth/token` handles the authorization code, refresh token and client credentials grants.
- `POST /oauth/revoke` ends a token.

A token goes in the Bearer `Authorization` header and acts as the user who consented, or as the application's own user for an app token.

The simulator creates no applications, so seed yours. Give it a client id and secret, its redirect URIs, whether `client_credentials` is on, the user its app tokens act as (`app_actor`) and the user who approves consent (`consent_actor`):

```bash
curl -s "$RYSTIC_URL/_rystic/seed" -H 'Content-Type: application/json' -o /dev/null -w '%{http_code}\n' -d '{"state_user": {"5b0a4d00-05e2-4a00-8b00-0000000000aa": {"id": "5b0a4d00-05e2-4a00-8b00-0000000000aa", "name": "Ana Ruiz", "displayName": "ana", "email": "ana@example.com", "active": true}}, "state_oauth_application": {"app_1": {"id": "app_1", "name": "Triage bot", "client_id": "cid_triage", "client_secret": "cs_triage", "redirect_uris": ["http://localhost:3000/callback"], "client_credentials": true, "app_actor": "5b0a4d00-05e2-4a00-8b00-0000000000aa", "consent_actor": "5b0a4d00-05e2-4a00-8b00-0000000000aa"}}}'
# -> 204
```

### Consent and the code grant

`GET /oauth/authorize` with a registered `redirect_uri` approves at once and redirects there with a `code` and your `state`. The consenting user is the request's `X-Rystic-Actor`, or else the application's `consent_actor`. Exchange the code once, as a form, with the client's secret, or with the `code_verifier` under PKCE (`plain` or `S256`):

```bash
CODE=$(curl -s -o /dev/null -w '%{redirect_url}' "$RYSTIC_URL/oauth/authorize?client_id=cid_triage&redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fcallback&response_type=code&scope=read,write&state=xyz" | sed 's/.*code=\([^&]*\).*/\1/')
curl -s "$RYSTIC_URL/oauth/token" -d grant_type=authorization_code -d code="$CODE" -d redirect_uri=http://localhost:3000/callback \
  -d client_id=cid_triage -d client_secret=cs_triage > grant.json
jq -c '{token_type, expires_in, scope, access: (.access_token | startswith("lin_oauth_")), refresh: (.refresh_token | startswith("lin_refresh_"))}' grant.json
# -> {"token_type":"Bearer","expires_in":86399,"scope":"read write","access":true,"refresh":true}
curl -s "$RYSTIC_URL/graphql" -H "Authorization: Bearer $(jq -r .access_token grant.json)" -H 'Content-Type: application/json' \
  -d '{"query":"{ viewer { name } }"}' | jq -c .data
# -> {"viewer":{"name":"Ana Ruiz"}}
```

A spent, unknown or expired code (codes live 10 minutes), or another `redirect_uri`, returns 400 `invalid_grant`.

### Refresh and its 30-minute replay

A refresh token is single-use: it mints a new access token and a new refresh token. The client authenticates in the form or with HTTP Basic. Replaying a spent refresh token within 30 minutes returns the same new pair again, so a client that lost the response can recover it. After 30 minutes the replay returns 400 `invalid_grant`, "Invalid grant: refresh token is invalid":

```bash
R=$(jq -r .refresh_token grant.json)
FIRST=$(curl -s "$RYSTIC_URL/oauth/token" -u cid_triage:cs_triage -d grant_type=refresh_token -d refresh_token="$R" | jq -r .access_token)
AGAIN=$(curl -s "$RYSTIC_URL/oauth/token" -u cid_triage:cs_triage -d grant_type=refresh_token -d refresh_token="$R" | jq -r .access_token)
[ "$FIRST" = "$AGAIN" ] && echo same pair
# -> same pair
```

An access token lives 24 hours (`expires_in` 86399) and an app token 30 days, both on the simulator clock. Move the clock with `POST /_rystic/clock` to test expiry without waiting.

### App tokens

With `client_credentials` on, the grant mints an app token that acts as `app_actor` and comes with no refresh token. Asking again under other scopes revokes the application's earlier app tokens, as Linear does:

```bash
curl -s "$RYSTIC_URL/oauth/token" -d grant_type=client_credentials -d scope=read,write -d client_id=cid_triage -d client_secret=cs_triage > app.json
jq -c '{token_type, expires_in, scope, refresh_token}' app.json
# -> {"token_type":"Bearer","expires_in":2591999,"scope":"read write","refresh_token":null}
```

### Revocation and a dead token

`POST /oauth/revoke` takes the token in the `token` field, in one legacy `access_token` or `refresh_token` field or as the Bearer. It ends the token and its refresh token. A refresh token never authenticates a request. A revoked or expired token then returns Linear's 401 `AUTHENTICATION_ERROR` on every request:

```bash
curl -s "$RYSTIC_URL/oauth/revoke" -d token="$(jq -r .access_token app.json)" -o /dev/null -w '%{http_code}\n'
# -> 200
curl -s "$RYSTIC_URL/graphql" -H "Authorization: Bearer $(jq -r .access_token app.json)" -H 'Content-Type: application/json' \
  -d '{"query":"{ viewer { name } }"}' | jq -c '.errors[0].extensions.code'
# -> "AUTHENTICATION_ERROR"
```

These are the other refusals Linear returns:

- A token body that is not a form: 400 `invalid_request`.
- No client: 400 `invalid_client`, "Invalid client: cannot retrieve client credentials".
- An unknown client or a wrong secret: 400 `invalid_client`, "Invalid client: client is invalid". Under HTTP Basic it is 401, with `WWW-Authenticate: Basic realm="Service"`.
- `POST /oauth/revoke` with no token: 401. A token it never issued: 401 "Token not found". `token` beside a legacy field, or both legacy fields: 400.
- A personal key sent under `Bearer`: 400 `INPUT_ERROR`, "It looks like you're trying to use an API key as a Bearer token."

A token the simulator did not issue is still accepted under `Bearer`, as every placeholder key is. The responses to an already revoked token, an unknown grant type, an invalid code and an application without `client_credentials` are not yet confirmed against Linear: their status and wording may differ.

---

Source: https://www.rystic.ai/docs/simulators/linear/features/oauth · Markdown: https://www.rystic.ai/docs/simulators/linear/features/oauth.md
