# User info

Read the claims the user approved for your app.

The OpenID Connect userinfo endpoint. Call it with an access token and you get
back exactly the claims the granted scopes cover — nothing is emitted "for
convenience".

**GET** `/api/auth/oauth2/userinfo`

`POST` works too, with the same header and the same response.

## Authorizations

| Name | Type | In | Required | Description |
| --- | --- | --- | --- | --- |
| `Authorization` | string | header | yes | `Bearer YOUR_ACCESS_TOKEN`. The token's granted scopes must include `openid`, otherwise the endpoint answers `400 invalid_scope` no matter what else it carries. |

## Claims

Every claim hangs off a scope. `sub` is the only one you always get.

| Claim | Scope | Example | What it is |
| --- | --- | --- | --- |
| `sub` | always | `clz9k2x0a0000s601f8h3d7q2` | The stable haunt.gg user id. It never changes — key your own user records on this, not on the username. |
| `uid` | `identify` | `1337` | The short numeric id shown on the profile. |
| `name` | `identify` | `John Doe` | The name shown on the profile. Free text — not unique and not a handle. |
| `preferred_username` | `identify` | `john` | The handle in the profile URL. Always lowercase, unique, and the user can change it. |
| `picture` | `identify` | `https://r2.haunt.gg/avatar/abc123.png` | The avatar of the user's active profile. |
| `profile` | `identify` | `https://haunt.gg/john` | The user's public page. |
| `email` | `email` | `john@example.com` | The address the user signs in with. |
| `email_verified` | `email` | `true` | Whether they confirmed it. |
| `connections` | `connections` | see below | Array of `provider` / `handle` pairs. Only `discord` and `lastfm` exist today. |

These are the OpenID Connect standard claim names, so any OIDC client library
fills in its user object on its own — there is nothing haunt-specific to map.
`uid` is the one claim the standard has no name for.

## Example Request

  #### cURL

```bash
curl "https://haunt.gg/api/auth/oauth2/userinfo" \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"
```
  #### JavaScript

```ts
const res = await fetch("https://haunt.gg/api/auth/oauth2/userinfo", {
  headers: { Authorization: `Bearer ${accessToken}` },
});
const user = await res.json();
```
  #### Python

```python
import requests

res = requests.get(
    "https://haunt.gg/api/auth/oauth2/userinfo",
    headers={"Authorization": f"Bearer {access_token}"},
)
user = res.json()
```

## Response

  #### 200 — Every scope granted. With openid identify you get everything down to profile; email and connections appear solely with their own scope.

```json
{
  "sub": "clz9k2x0a0000s601f8h3d7q2",
  "uid": 1337,
  "name": "John Doe",
  "preferred_username": "john",
  "picture": "https://r2.haunt.gg/avatar/abc123.png",
  "profile": "https://haunt.gg/john",
  "email": "john@example.com",
  "email_verified": true,
  "connections": [
    { "provider": "discord", "handle": "1363890885443452989" },
    { "provider": "lastfm", "handle": "john" }
  ]
}
```
  #### 200 — The account was banned or hidden after the token was issued. Everything except sub is withheld, and email is explicitly nulled — treat this as an account that no longer exists and stop using the token.

```json
{
  "sub": "clz9k2x0a0000s601f8h3d7q2",
  "email": null,
  "email_verified": false
}
```
  #### 400 — The token is missing the openid scope, or the user behind it can no longer be resolved.

```json
{
  "error": "invalid_scope",
  "error_description": "Missing required scope"
}
```
```json
{
  "error": "invalid_request",
  "error_description": "user not found"
}
```
  #### 401 — No Authorization header, or the access token is unknown or expired.

```json
{
  "error": "invalid_request",
  "error_description": "authorization header not found"
}
```

## Or just read the id_token

If all you need is identity, you do not have to call this endpoint at all — the
`id_token` from the token response already carries the `identify` and `email`
claims. It deliberately leaves out the bulkier `connections`, which only the
userinfo endpoint returns.

The token is a JWT signed with **EdDSA** (Ed25519). Verify it against the JWKS
at `https://haunt.gg/api/auth/jwks`, and check that `iss` is `https://haunt.gg`,
that `aud` is your `client_id`, and that `nonce` matches the one you sent.

  #### id_token — Decoded payload for a token granted openid identify email. auth_time is when the user actually signed in; the token itself is valid for 10 hours.

```json
{
  "iss": "https://haunt.gg",
  "sub": "clz9k2x0a0000s601f8h3d7q2",
  "aud": "YOUR_CLIENT_ID",
  "iat": 1767222000,
  "exp": 1767258000,
  "auth_time": 1767221900,
  "acr": "urn:mace:incommon:iap:bronze",
  "nonce": "YOUR_NONCE",
  "uid": 1337,
  "name": "John Doe",
  "preferred_username": "john",
  "picture": "https://r2.haunt.gg/avatar/abc123.png",
  "profile": "https://haunt.gg/john",
  "email": "john@example.com",
  "email_verified": true
}
```

## Rate limit

100 requests per minute per IP address, counted for this endpoint alone.
