# ID token & UserInfo

> The id_token claims — including amr (how the user authenticated) and auth_time — and the UserInfo endpoint (GET|POST /api/oauth/userinfo) that returns scope-gated identity claims.

OpenID Connect gives you two sources of identity: the **`id_token`** returned by the [token endpoint](/docs/oauth/token/) (a signed statement about *who* signed in and *how*), and the **UserInfo endpoint** (a live, server-verified lookup of the user's claims). Both are gated by the scopes the user granted.

## The `id_token`

Issued when the `openid` scope is granted. It is an **RS256 JWT** (`typ: JWT`) that verifies offline against the [JWKS](/docs/oauth/discovery-jwks/).

### Claims

| Claim | Always | Meaning |
|---|---|---|
| `iss` | ✓ | Issuer — `https://entrybit.net`. |
| `sub` | ✓ | Stable EntryBit user identifier. |
| `aud` | ✓ | Your `client_id`. |
| `iat` | ✓ | Issued-at (Unix seconds). |
| `exp` | ✓ | Expiry (Unix seconds). |
| `nonce` |  | Echoed from your authorize request — verify it matches. |
| `auth_time` |  | When the user actually authenticated (Unix seconds). |
| `amr` |  | **How** the user authenticated (see below). |
| `at_hash` |  | Binds the id_token to the access token. |
| `email`, `email_verified` |  | With the `email` scope. |
| `name`, `picture` |  | With the `profile` scope. |

### `amr` — how the user signed in

The `amr` (Authentication Methods References, [RFC 8176](https://www.rfc-editor.org/rfc/rfc8176)) array records the factors used at login. EntryBit stamps these values:

| Value | Meaning |
|---|---|
| `pwd` | Password. |
| `otp` | One-time code (authenticator / email). |
| `sms` | SMS one-time code. |
| `swk` | Passkey (software-backed key). |
| `user` | Passkey with user verification. |
| `fed` | Federated login (Google). |
| `mfa` | Two or more factors were used. |

For example, `["swk","user"]` is a verified passkey, `["pwd","otp","mfa"]` is password plus a code, and `["fed"]` is Google. Combined with `auth_time`, this lets a sensitive screen require a *recent* or *stronger* login — request one with `prompt=login` or `max_age` on the [authorize request](/docs/oauth/authorize/).

### Validating it yourself

If you decode the `id_token` on the client, **fully validate the JWT** — do not trust decoded claims blindly:

1. Verify the **signature** against [`/.well-known/jwks.json`](/docs/oauth/discovery-jwks/).
2. Check `iss` == `https://entrybit.net`.
3. Check `aud` == your `client_id`.
4. Check `exp` is in the future.
5. Check `nonce` equals the value you sent (so **make `nonce` required** if you consume the id_token). Many libraries — including `expo-auth-session` — do **not** validate id_tokens for you.

With [`jose`](https://github.com/panva/jose) in Node.js, steps 1–4 are options and step 5 is yours:

```ts
// npm install jose
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://entrybit.net/.well-known/jwks.json'));

export async function verifyIdToken(idToken: string, expectedNonce: string) {
  // Steps 1–4 — signature, iss, aud, exp — enforced by jose.
  const { payload } = await jwtVerify(idToken, JWKS, {
    issuer: 'https://entrybit.net',
    audience: CLIENT_ID,
    algorithms: ['RS256'],
  });

  // Step 5 — the nonce binds this id_token to YOUR authorize request.
  if (payload.nonce !== expectedNonce) {
    throw new Error('id_token nonce mismatch — possible replay');
  }

  return payload; // { sub, amr?, auth_time?, email?, name?, … }
}
```

If you only need the user's profile, prefer UserInfo below — it is already server-verified.

## UserInfo endpoint

Returns the signed-in user's claims, gated by the granted scopes. Available over GET and POST (OIDC Core §5.3.1 — the two are identical).

```http
GET  /api/oauth/userinfo
POST /api/oauth/userinfo
Authorization: Bearer <access_token>
```

Requires the `openid` scope. The `email` and `profile` scopes gate their respective claims.

```bash
curl https://entrybit.net/api/oauth/userinfo \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

### Response

```json
{
  "sub": "usr_9f1c2ab34cd56ef7",
  "email": "dana@example.com",
  "email_verified": true,
  "name": "Dana Cohen",
  "picture": "https://entrybit.net/…"
}
```

| Field | Present when |
|---|---|
| `sub` | Always (with `openid`). |
| `email`, `email_verified` | The `email` scope was granted. |
| `name`, `picture` | The `profile` scope was granted. |

### Errors

`401`/`403` responses carry a `WWW-Authenticate: Bearer` challenge:

| Status | `error` | Meaning |
|---|---|---|
| `401` | `invalid_token` | Missing, expired, or invalid access token — [refresh](/docs/oauth/token/) or re-login. |
| `403` | `insufficient_scope` | The token lacks `openid`. |

See [Errors](/docs/api-reference/errors/) for the full Bearer-challenge contract.