# Create an OAuth app and the consent screen

> Step 3: register an OAuth application, send the person to the consent screen with PKCE and SMART v2 scopes, exchange the code for a 30-minute token, refresh and revoke, with copyable JavaScript and Python.

Source: https://developers.anpheros.com/guides/oauth-app

Step 3 of 5. An API key is for your server and your own test data. When a real person uses your
app, the person signs in with Anpheros, sees a consent screen with your app's name, what it asks
for and for how long, and approves it. Your app then gets a token for that one person's record,
limited to what they approved. This is OAuth 2.1 with PKCE, in the SMART on FHIR format.

**In short:**

| Step | Call | Who |
|---|---|---|
| 1. Register the app | `POST /admin/projects/{project_id}/applications` | you, once |
| 2. Send the person to consent | `GET /oauth/authorize?…` | your app opens the browser |
| 3. The person approves | consent screen: scopes, purpose, 30/90/180/365 days | the person |
| 4. Exchange the code | `POST /oauth/token` | your app |
| 5. Call the API | `Authorization: Bearer at_…` | your app |

<!-- visual:seq-oauth -->

## 1. Register your app

In the dashboard, or with the Admin API and your dashboard user token (a Firebase ID token):

```bash
curl -X POST https://platform.anpheros.com/admin/projects/proj_…/applications \
     -H "Authorization: Bearer $FIREBASE_ID_TOKEN" -H 'content-type: application/json' -d '{
  "name": "CardioNotes",
  "client_type": "public",
  "redirect_uris": ["com.cardionotes.app:/oauth/callback", "http://localhost:5173/callback"],
  "purpose": "Shows your blood pressure readings and lets you add new ones."
}'
```

```json
{"id":"app_…","client_id":"client_…","client_type":"public","redirect_uris":["com.cardionotes.app:/oauth/callback","http://localhost:5173/callback"],"status":"active","…":"…"}
```

- `client_type`: `public` (the default) for a mobile app, a single-page app or anything that cannot
  keep a secret; `confidential` for a backend. A confidential app gets a `client_secret` **once**, in
  this answer.
- `redirect_uris`: 1 to 10 addresses, each `https://…`, a custom scheme for a mobile app, or
  `http://localhost…` for development. The address in the request must match one of them exactly.
- `purpose` (up to 500 characters) is shown to the person on the consent screen. Add a logo and a
  colour with `PUT /admin/applications/{id}/branding`.

## 2. Send the person to the consent screen

Create a PKCE pair and a `state`, keep both, and open the authorize address in the browser:

```js
// in the browser or in Node 18+
const b64url = buf => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));          // 43 characters
const challenge = b64url(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)));
const state = crypto.randomUUID();

const url = new URL('https://platform.anpheros.com/oauth/authorize');
url.search = new URLSearchParams({
  response_type: 'code',
  client_id: 'client_…',
  redirect_uri: 'http://localhost:5173/callback',
  scope: 'patient/Observation.rs patient/Observation.c offline_access',
  state,
  code_challenge: challenge,
  code_challenge_method: 'S256',
}).toString();
location.href = url;   // keep verifier and state until the person comes back
```

```python
import base64, hashlib, secrets, urllib.parse

verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
state = secrets.token_urlsafe(16)
print("https://platform.anpheros.com/oauth/authorize?" + urllib.parse.urlencode({
    "response_type": "code", "client_id": "client_…", "redirect_uri": "http://localhost:5173/callback",
    "scope": "patient/Observation.rs patient/Observation.c offline_access",
    "state": state, "code_challenge": challenge, "code_challenge_method": "S256"}))
```

**Scopes** use the SMART v2 format `patient/<Type>.<cruds>`: `r` read, `s` search, `c` create,
`u` update, `d` delete. Examples: `patient/Observation.rs`, `patient/Condition.rs`,
`patient/*.rs` (read everything), `patient/Observation.rs?category=laboratory` (lab results only).
Add `offline_access` for a refresh token and `openid fhirUser` for an `id_token`. An unknown scope
is refused with `400`, not ignored. Discovery document:
`https://platform.anpheros.com/.well-known/smart-configuration`.

## 3. What the person sees

The consent screen shows your app's name, organization, logo and privacy link, the scopes in plain
language, the purpose, whose record (their own or a family member's) and **for how long**: 30, 90,
180 or 365 days, 90 by default. In the sandbox the screen carries a "TEST · Sandbox" banner.

If the person approves, the browser goes to your `redirect_uri` with `?code=…&state=…`. Check that
`state` is the one you sent. The code is valid for 10 minutes. If they decline, you get
`?error=access_denied&state=…`.

## 4. Exchange the code for a token

```bash
curl -X POST https://platform.anpheros.com/oauth/token \
     -d grant_type=authorization_code -d code="$CODE" -d code_verifier="$VERIFIER" \
     -d redirect_uri=http://localhost:5173/callback -d client_id=client_…
```

```json
{"access_token":"at_…","token_type":"Bearer","expires_in":1800,"scope":"patient/Observation.rs patient/Observation.c offline_access","patient":"…","grant_id":"…","refresh_token":"rt_…"}
```

- The access token lasts 30 minutes; the refresh token 30 days.
- `patient` is the id of the person's record **for your app**: another app sees the same person
  under a different id.
- A confidential app also sends `client_secret` (or HTTP Basic authentication).

## 5. Call the API, refresh and revoke

```bash
curl "https://platform.anpheros.com/v1/patients/$PATIENT/observations?limit=10" -H "Authorization: Bearer $ACCESS_TOKEN"

# when the access token expires
curl -X POST https://platform.anpheros.com/oauth/token \
     -d grant_type=refresh_token -d refresh_token="$REFRESH_TOKEN" -d client_id=client_…

# when the person signs out of your app
curl -X POST https://platform.anpheros.com/oauth/revoke -d token="$REFRESH_TOKEN" -d client_id=client_…
```

Refresh tokens rotate: each refresh returns a new one, and reusing an old one revokes the whole
chain (a sign that a token was stolen). The person can also revoke your app at any time from their
Connections in Anpheros; your next call then gets `401` `The consent behind this token is no longer
active.` Both SDKs implement this whole flow, including PKCE and refresh: see
[Authentication and OAuth](https://developers.anpheros.com/guides/authentication).

## Common errors

| You get | Fix |
|---|---|
| `400` `redirect_uri is not registered for this client.` | Register the exact `redirect_uri`, with the same scheme, host, port and path |
| `400` on an unknown scope | Check the spelling: `patient/Observation.rs`, not `patient/Observation.read` |
| `invalid_grant` `Authorization code already used or expired.` / `PKCE verification failed.` | The code expired (10 minutes) or was already used, or the `code_verifier` is not the one behind the `code_challenge` |
| `403` `The consent behind this token does not allow 'write' on Observation.` | Ask for `patient/Observation.c` in the scope |

## Next step

Step 4: instead of asking every few minutes whether something changed, let the platform tell you:
[Receive your first webhook](https://developers.anpheros.com/guides/first-webhook).

## Frequently asked questions

### Can a mobile app keep a client secret?
No. Register a mobile or browser app as `public` and use PKCE, which is required for every app. Only a backend should be `confidential`.

### How long does the person's approval last?
As long as they chose on the consent screen: 30, 90, 180 or 365 days, 90 by default. They can withdraw it earlier at any time.

### Do I need OAuth for my own test data?
No. For the sandbox and for data your own organization manages, use an API key on your server. OAuth is for acting on behalf of a person.
