# API Authentication

For the full security picture — XSS prevention, DynamoDB isolation, SSRF, IAM — see `docs/security.md`. This document covers the API authentication flow specifically.

## Flow

The SPA authenticates API requests using ATProto service JWTs:

1. After OAuth sign-in, the browser calls `com.atproto.server.getServiceAuth` on the user's PDS via `session.fetchHandler`. The PDS issues a short-lived service JWT (`aud` = `VITE_API_DID`, `iss` = user DID, signed with the user's atproto signing key, ~60 s expiry).
2. The browser sends an authenticated request to the API (e.g. `GET /posts`, `POST /posts`, `DELETE /posts`, `POST /publications`, `PUT /publications`, `DELETE /publications`) with `Authorization: Bearer <service JWT>`.
3. The API Lambda verifies the JWT: resolves the user's DID document via `@atproto/identity` `IdResolver`, checks the signature with `@atproto/crypto` `verifySignature`, and validates `aud` and `exp`.
4. On success, the API Lambda reads from or writes to DynamoDB using its own IAM role and returns the result.

## Security

**No AWS credentials in the browser.** The API Lambda holds IAM permissions; the browser only holds the ATProto session.

**Service JWT verification.** The API Lambda resolves the issuer DID to get the user's atproto signing key, then verifies the JWT signature. This proves the token was issued by the claimed user's PDS.

**Audience check.** The API Lambda rejects any JWT whose `aud` doesn't match `API_DID`. This prevents tokens issued for other services from being replayed here.

**Short expiry.** Service JWTs expire in ~60 seconds. A new one is fetched before each authenticated API call.

## API DID and `did:web`

`API_DID` is the identifier of this API service. It appears as the `aud` claim in service JWTs — the PDS encodes whatever string the client requests, and the API Lambda checks it matches.

**For local development** (`did:web:localhost`): the DID does not need to be resolvable. The API Lambda checks `aud` as a plain string comparison; neither side resolves the API's DID document.

**For production**, use `did:web:<yourdomain.com>` and serve a DID document so that:
1. The `rpc:` scope system can validate the audience (see `docs/atproto-auth.md`)
2. Other ATProto services and clients can discover the API's service endpoint

### Why `did.json` has no `verificationMethod`

A `verificationMethod` declares a public key so others can verify signatures made by the DID owner. It is required for signing authorities — PDSes, user identities — but this app is not one.

`public/.well-known/did.json` has a single purpose: advertising the `#blog-api` service endpoint so the PDS can resolve `did:web:<domain>` and confirm the service reference is valid when evaluating the `rpc:` OAuth scope. The API Lambda verifies service JWTs by resolving the *user's* DID (to get the user's atproto signing key) — not the app's DID. Lemma never signs anything that would require a counterparty to look up its public key, so no `verificationMethod` is needed.

`public/.well-known/did.json` already exists in the repo with `YOUR_DOMAIN` placeholders. Run `make -C infra setup-aws-domain DOMAIN_NAME=yourdomain.com` to update it (along with `public/oauth-client-metadata.json`) for your domain before building.

Use the CloudFront API URL (`ApiUrl` output of the cloudfront stack, or `api.<domain>` after Route 53 is set up) rather than the raw API Gateway URL. If CloudFront is not deployed, use `https://<api-gateway-id>.execute-api.<region>.amazonaws.com/prod` instead.

Then update:
- `VITE_API_DID = did:web:yourdomain.com` (build-time env var — the OAuth scope is derived from this automatically)
- `ApiDid = did:web:yourdomain.com` (CloudFormation parameter)
- `getServiceAuth` `aud` param in `app/lib/api-client.ts`: `did:web:yourdomain.com#blog-api`
- API Lambda `API_DID` env var: `did:web:yourdomain.com#blog-api` (must include fragment when using `rpc:` scopes)

## API Lambda deployment

The CloudFormation template creates the function with placeholder code. Deploy the real implementation separately:

```bash
make -C infra update-local-api          # LocalStack
make -C infra update-aws-api            # AWS
```

The API Lambda is built by esbuild, which bundles `@atproto/identity`, `@atproto/crypto`, and `@aws-sdk/lib-dynamodb` into a single `dist/index.js`.

## Environment variables

| Variable | Where | Description |
|----------|-------|-------------|
| `VITE_API_URL` | Build-time (Vite) | Base URL of the blog API |
| `VITE_API_DID` | Build-time (Vite) | DID of the API service — `aud` for service JWTs |
| `API_DID` | API Lambda runtime | Must match `VITE_API_DID`; set via CloudFormation `ApiDid` parameter |
| `DYNAMODB_TABLE` | API Lambda runtime | DynamoDB table name; set via CloudFormation parameter |
