# The authorization flow

Haven implements OAuth 2.1 authorization code with PKCE. If you have integrated with a modern OAuth provider this will hold no surprises, and a standard client library will work. The details that differ are collected at the end.

## The shape

A host connects your application in five steps.

You send them to `/oauth/authorize` with your client id, a redirect URI, the scopes you want and a PKCE challenge. Haven authenticates them, shows what you are asking for, and they approve or decline. On approval Haven redirects back to you with a single-use authorization code. You exchange that code, plus the PKCE verifier, for an access token and a refresh token. From then on you call the API with the access token and rotate it with the refresh token.

## Discovery

Haven publishes RFC 8414 metadata, so a library can configure itself:

```
GET https://www.bookwithhaven.com/.well-known/oauth-authorization-server
```

It also publishes RFC 9728 protected-resource metadata, at the root and at the path-inserted form, which is what lets an MCP client discover the authorization server from nothing but a 401:

```
GET https://www.bookwithhaven.com/.well-known/oauth-protected-resource
GET https://www.bookwithhaven.com/.well-known/oauth-protected-resource/api/public/v1
```

Two absences in the metadata are deliberate and worth reading as answers rather than omissions. There is no `registration_endpoint`, because Haven has no dynamic client registration: every client is created by a person after review. There is no `jwks_uri`, because tokens are opaque rather than signed.

## Endpoints

| Purpose    | Endpoint                            |
| ---------- | ----------------------------------- |
| Authorize  | `GET /oauth/authorize`              |
| Token      | `POST /api/public/oauth/token`      |
| Revoke     | `POST /api/public/oauth/revoke`     |
| Introspect | `POST /api/public/oauth/introspect` |

## Tokens are opaque

An access token is `hvat_` followed by random bytes. It is not a JWT, carries no claims, and cannot be inspected offline.

The reason is revocation. Haven recomputes what a token may do on every single request, intersecting the token's scopes with the host's current grant and with the ceiling Haven staff set on your application. A host narrowing your access, an agency link being severed, a seat being revoked, or Haven reducing your approved scopes all take effect on your very next call, with no reissue and no propagation delay. A signed token would need a revocation list to achieve the same thing, which is the database lookup this design already performs.

The practical consequence for you: do not cache authorization decisions, and read the `scope` field on every token response.

## What differs from a typical provider

**PKCE is mandatory for every client**, including confidential ones holding a secret. `S256` only. `plain` is rejected, and `code_challenge_method` is required rather than defaulted.

**`state` is required.** Formally it is optional when PKCE is present. Haven requires it because it is the only CSRF binding your own client has for its session, and every real library sends it.

**Authorization codes live sixty seconds** and are consumed on first presentation, before the PKCE check runs. A failed exchange cannot be retried; re-run the authorization instead. This bounds a stolen code to a single verifier attempt.

**Refresh tokens rotate.** Every refresh returns a new refresh token and invalidates the old one, with a sixty-second grace window for concurrent callers. Presenting a rotated token after that window is treated as theft and suspends the grant. See [Refreshing and rotation](04-refreshing-and-rotation.md).

**`redirect_uri` is matched by exact string equality.** No normalization, no trailing-slash tolerance, no wildcards, no subdomain matching. The single exception is the port on a loopback address.

**Errors from the OAuth endpoints use the RFC shape**, `{"error": "...", "error_description": "..."}`, not Haven's envelope. Everything under `/api/public/v1` uses the envelope. The boundary is exactly where a generic OAuth library stops reading and your own code starts.

## Sequence

```
  Your app                     Host's browser              Haven
     |                              |                        |
     |-- authorize URL ------------>|                        |
     |                              |-- GET /oauth/authorize->|
     |                              |<-- consent screen ------|
     |                              |-- approve ------------->|
     |<-- 302 ?code=&state=&iss= ---|                        |
     |                                                       |
     |-- POST /token (code + verifier + client auth) -------->|
     |<-- access_token, refresh_token, scope ----------------|
     |                                                       |
     |-- GET /api/public/v1/... (Bearer access_token) ------->|
     |<-- {success, value, error} ---------------------------|
```

Check `state` against what you sent before you use the code. Check `iss` equals `https://www.bookwithhaven.com` if your library supports RFC 9207; it is how you detect a mix-up attack when your client talks to more than one provider.
