# ATProto Authentication

## Package

`@atproto/oauth-client-browser` — handles PKCE, DPoP key generation, token storage in IndexedDB, and automatic token refresh.

## Client metadata

The OAuth client requires a JSON document hosted at a publicly reachable URL that becomes the `client_id`. It must be served from S3 alongside the SPA (e.g. `https://<bucket>/oauth-client-metadata.json`). The ATProto authorization server fetches this URL to verify the client's declared redirect URIs and grant types before completing the OAuth flow.

For local development the library detects loopback origins and uses a built-in development client, so no hosted metadata is needed.

## Handle resolution

Browsers cannot perform DNS TXT lookups, so the OAuth client needs an HTTP resolver to turn a Bluesky handle (e.g. `alice.bsky.social`) into a DID. The app currently uses `https://bsky.social` as the resolver, which is the simplest option but leaks user handles and IPs to Bluesky.

To avoid this, replace `handleResolver` in `app/lib/oauth-client.ts` with a DNS-over-HTTPS provider:

```ts
handleResolver: 'https://cloudflare-dns.com/dns-query'
```

## Session lifecycle

Managed in `app/lib/auth-context.tsx`:

1. `client.init()` on page load — restores any existing session from IndexedDB and handles the post-redirect OAuth callback.
2. `client.signIn(handle)` — initiates the OAuth redirect; the returned promise never resolves.
3. `{ onDelete }` hook — fires when the ATProto server revokes the session (e.g. user signed out from another device). Used to clear React state.
4. `session.signOut()` — revokes the token and clears local storage.
5. Token refresh is handled automatically by the library using DPoP-bound refresh tokens.

## DPoP

All ATProto access tokens are DPoP-bound. The library generates an in-memory key pair and attaches a proof JWT to every request made via `session.fetchHandler`. The proof binds the request to a specific HTTP method and URL, making stolen tokens useless without the private key.

When calling the PDS (e.g. `getServiceAuth`, `putRecord`, `createRecord`, `deleteRecord`) always use `session.fetchHandler` rather than plain `fetch` — this ensures the DPoP proof is included and the token can be verified as legitimately used by this session.

## OAuth scopes

`oauth-client.ts` derives the scope from `VITE_API_DID` at build time — no manual change is needed when switching environments:

- **Local dev** (`VITE_API_DID=did:web:localhost`) — uses `transition:generic` because the PDS cannot reach `localhost` to validate a `did:web` service reference.
- **Production** (any other `VITE_API_DID`) — uses `repo:` scopes for per-collection write access and `rpc:` scopes for authenticated API calls:

```
atproto
repo:site.standard.publication
repo:site.standard.document
repo:site.standard.graph.subscription
repo:app.bsky.feed.post?action=create&action=update&action=delete
repo:app.bsky.feed.like
rpc:com.atproto.server.getServiceAuth?aud=*
rpc:*?aud=<VITE_API_DID>%23blog-api
```

`repo:` scopes restrict write access to specific record collections only. `app.bsky.feed.post` covers all three actions — created on first publish, updated via `putRecord` when an already-published article is edited, and deleted when the article is deleted. Read operations (`getRecord`, `listRecords`) are public on ATProto and need no auth scope. `rpc:com.atproto.server.getServiceAuth?aud=*` uses `aud=*` because each user's PDS has a different DID. `setup-aws-domain` writes the resolved scope (with the real domain) into `public/oauth-client-metadata.json`.

### `rpc:` scope format

```
rpc:<lxm>?aud=<did-service-ref>
```

- **`lxm`** — the NSID of the operation being authorized. Wildcard `*` authorizes all operations on the target service.
- **`aud`** — a DID plus a `#fragment` identifying a specific service entry in that DID's document, e.g. `did:web:yourdomain.com#blog-api`. Wildcard `*` is allowed but `lxm` and `aud` cannot both be wildcards.

The granular scope requires `did:web:<domain>` to be resolvable (i.e. `did.json` served from the site origin) so the PDS can validate the `aud` claim against the DID document's service entries. See `docs/api-auth.md` for `did:web` setup.

For a full account of XSS mitigations, DynamoDB isolation, SSRF prevention, and other security measures see `docs/security.md`.
