# Authentication

Every request to `/v1/*` must carry a bearer token:

```
Authorization: Bearer <token>
```

Two token kinds are accepted:

| Prefix | Type | Use for |
|---|---|---|
| `42min_pat_…` | Personal access token (PAT) | Server-side scripts and integrations that act as **your own account**. |
| `42min_at_…` | OAuth 2.1 access token | A **third-party app** acting on behalf of a 42min user. |

There is no other auth surface — cookies, session JWTs, and Basic auth are not
accepted on `/v1/*`. Missing or malformed credentials return `401 invalid_token`
with a `WWW-Authenticate: Bearer …` header.

## Personal access tokens (PATs)

PATs are long-lived bearer tokens scoped to your own account. Use them for
internal scripts, cron jobs, server-side automations — anywhere the caller *is*
you.

### Token format

```
42min_pat_<lookup12>_<secret32>
```

- `42min_pat_` — fixed prefix that identifies the token family.
- `<lookup12>` — 12-character public lookup id (Crockford base32). Used only to
  find the row; not a secret.
- `<secret32>` — 32-character secret. Hashed at rest with HMAC-SHA256 keyed by
  a server pepper. **Shown once at creation** — store it immediately.

### Mint a PAT

1. Open [Admin Center → API](/360/api) and switch to the **API keys** tab. You
   need the **Owner** or **Admin** role.
2. Click **New API key**. Give it a name, pick the [scopes](/help/api/scopes)
   you want it to hold, and optionally set an expiry.
3. Copy the token that appears — it is shown **once** and never again. The
   dashboard only ever displays the last four characters afterward.

Limits:

- **42 active keys** per account.
- Key names are unique per account (case-insensitive).
- Adding scopes to an existing key requires re-confirming your account
  password.

```bash
curl -H "Authorization: Bearer 42min_pat_ABCDEFGHJKMN_PQRSTVWXYZ23456789ABCDEFGHJK" \
  https://api.42min.us/v1/me
```

### Rotation and revocation

- **Regenerate** issues a new secret on the same key id; the old secret is
  revoked immediately.
- **Delete** revokes a key. Once revoked, requests with that token return
  `401 token_revoked`.
- Expired keys return `401 token_expired`.

PATs are owned by the user who created them and inherit that user's account
membership. If the user is removed from the account, the keys they own stop
working.

## OAuth 2.1 + PKCE

For **third-party applications** that act on behalf of a 42min user, use the
OAuth 2.1 authorization code flow with **PKCE (S256)**. PKCE is mandatory for
every client — public and confidential alike.

Discovery is published at
[`/.well-known/oauth-authorization-server`](/help/api/discovery).

| Endpoint | Path |
|---|---|
| Authorization | `GET https://api.42min.us/v1/oauth/authorize` |
| Token | `POST https://api.42min.us/v1/oauth/token` |
| Revocation | `POST https://api.42min.us/v1/oauth/revoke` |
| Introspection | `POST https://api.42min.us/v1/oauth/introspect` |

### Register an OAuth client

1. In [Admin Center → API](/360/api), open the **OAuth apps** tab. Owner or
   Admin role required.
2. Click **New OAuth app** and fill in:
   - **Name** — shown to users on the consent screen.
   - **Client type** — `confidential` (server-side, can hold a secret) or
     `public` (browser/mobile, uses PKCE only).
   - **Redirect URIs** — exact match required at authorize time. `https://`
     only, except `http://localhost` and `http://127.0.0.1` for development.
   - **Allowed scopes** — the superset of [scopes](/help/api/scopes) the app
     can ever request. Per-grant scopes must be a subset of this list.
3. Save. You get back:
   - `client_id` — `42min_<24 chars>` — not secret.
   - `client_secret` — `42min_cs_<48 chars>`, **shown once** (confidential
     clients only).
   - **Rotate secret** issues a new one and revokes the old one immediately.

### The full flow

```text
your app ──► 1. /v1/oauth/authorize ──► 302 to 42min.us/oauth/consent
                                              │
                                              ▼
        user logs in & approves on consent screen
                                              │
                                              ▼
your app ◄── 2. 302 redirect_uri?code=42min_ac_…&state=…
your app ──► 3. POST /v1/oauth/token (code + code_verifier)
your app ◄── 4. { access_token: "42min_at_…", refresh_token: "42min_rt_…", expires_in: 3600 }
```

#### 1. Build the PKCE verifier and challenge

Generate a random `code_verifier` (43–128 URL-safe characters) and derive
`code_challenge = BASE64URL(SHA256(code_verifier))`. Persist the verifier in
the user's session — you need it at step 3.

#### 2. Send the user to `/v1/oauth/authorize`

```
https://api.42min.us/v1/oauth/authorize
  ?response_type=code
  &client_id=42min_…
  &redirect_uri=https://yourapp.example.com/callback
  &scope=event_types:read%20slots:read%20bookings:write
  &code_challenge=<challenge>
  &code_challenge_method=S256
  &state=<csrf-token>
```

Required parameters:

- `response_type=code` — the only response type supported.
- `client_id`
- `redirect_uri` — must exactly match one of the URIs registered on the client.
- `code_challenge` and `code_challenge_method=S256` — anything else is rejected.
- `scope` — space-separated list of scopes; must be a subset of the client's
  `allowed_scopes`.
- `state` — opaque value echoed back on redirect. Use it to bind the response
  to the user's session and to defend against CSRF.

42min redirects to its in-product consent screen, where the user signs in (if
needed) and approves or denies the request. After the decision, 42min redirects
to your `redirect_uri`:

```
https://yourapp.example.com/callback?code=42min_ac_…&state=<csrf-token>
```

On denial:

```
https://yourapp.example.com/callback?error=access_denied&state=<csrf-token>
```

Authorization codes live for **10 minutes** and are single-use.

#### 3. Exchange the code at `/v1/oauth/token`

```bash
curl -X POST https://api.42min.us/v1/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=authorization_code \
  --data-urlencode code=42min_ac_… \
  --data-urlencode redirect_uri=https://yourapp.example.com/callback \
  --data-urlencode client_id=42min_… \
  --data-urlencode code_verifier=<verifier> \
  --data-urlencode client_secret=42min_cs_…   # confidential clients only
```

Confidential clients may authenticate with HTTP Basic instead of body
parameters: `Authorization: Basic base64(client_id:client_secret)`.

Response:

```json
{
  "access_token": "42min_at_…",
  "refresh_token": "42min_rt_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "event_types:read slots:read bookings:create bookings:cancel bookings:reschedule bookings:update"
}
```

- **Access tokens** live for **1 hour**.
- **Refresh tokens** live for **60 days** and are **rotated on every use**.
- The `scope` claim reflects the *expanded* set (aliases like `bookings:write`
  are unrolled). See [Scopes](/help/api/scopes).

#### 4. Refresh

```bash
curl -X POST https://api.42min.us/v1/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode grant_type=refresh_token \
  --data-urlencode refresh_token=42min_rt_… \
  --data-urlencode client_id=42min_… \
  --data-urlencode client_secret=42min_cs_…
```

You get a **new** access token *and* a **new** refresh token. **Discard the
old refresh token immediately** — see reuse detection below.

### Refresh token reuse detection

Refresh tokens are one-shot. Every successful refresh marks the presented
refresh token as consumed and issues a new pair. If a previously-consumed (or
revoked) refresh token is ever presented again, the entire **grant family** —
every access and refresh token issued from the same authorization code — is
revoked immediately. The caller gets:

```json
{
  "error": "invalid_grant",
  "message": "Refresh token has already been used; the session has been revoked"
}
```

This protects against the case where an attacker captures a refresh token: the
moment the legitimate client or the attacker uses it a second time, the session
is killed and the user must re-authorize.

The same protection applies to **authorization codes** — replay of a consumed
code revokes the whole grant family with `error: invalid_grant`,
`message: "Authorization code already used"`.

### Revocation

```bash
curl -X POST https://api.42min.us/v1/oauth/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode token=42min_rt_… \
  --data-urlencode client_id=42min_… \
  --data-urlencode client_secret=42min_cs_…
```

Per RFC 7009 the endpoint always returns `200 OK`, even for unknown tokens.
Revoking a refresh token also revokes every access token in the same grant
family; revoking an access token revokes only that token.

### Introspection (confidential clients)

```bash
curl -X POST https://api.42min.us/v1/oauth/introspect \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode token=42min_at_… \
  --data-urlencode client_id=42min_… \
  --data-urlencode client_secret=42min_cs_…
```

Returns `{ "active": true, "scope": "…", "client_id": "…", "exp": …, … }` for
tokens **owned by the calling client**. Tokens belonging to a different client
(or expired/revoked/unknown tokens) return `{ "active": false }` — there is no
information leak across clients.

### Client types and token-endpoint auth

- **Confidential** clients authenticate with `client_secret_basic` or
  `client_secret_post`.
- **Public** clients (mobile, SPAs) authenticate with PKCE only —
  `token_endpoint_auth_method=none`.

### Token lifetimes — quick reference

| Token | TTL | Rotated on refresh |
|---|---|---|
| Authorization code | 10 minutes | — (single use) |
| Access token | 1 hour | — |
| Refresh token | 60 days | yes |
| PAT | unlimited unless `expiresAt` is set | — |

## Choosing PAT vs. OAuth

- Building a **personal script** or an integration that acts as a specific
  user — **PAT**.
- Building a **product that other 42min users install** to give your app
  access to their account — **OAuth**.

PATs are simpler but inherit one user's identity. OAuth is more work but gives
each end-user their own consent screen, their own scope set, and their own
revocation control.
