# auth.md — AI Daily

> Agent authentication walkthrough for AI Daily. Every read endpoint is anonymous-by-default — the OAuth surface below exists for agents that need a bearer token, an identity assertion, or a published `agent_auth` discovery block. Follows the [WorkOS auth.md spec](https://workos.com/auth-md): `agent_auth`, `register_uri`, `identity_assertion`, id-jag, `WWW-Authenticate`.

## Discover

Two ways to discover the auth surface, no scraping required:

1. **WWW-Authenticate challenge.** Probe the agent-auth challenge endpoint to receive a spec-shaped 401 that points at the protected-resource metadata:

   ```bash
   curl -i https://ai-daily.lugassy.net/agent/auth
   # HTTP/1.1 401 Unauthorized
   # WWW-Authenticate: Bearer realm="https://ai-daily.lugassy.net", scope="read:episodes read:transcripts search:episodes", resource_metadata="https://ai-daily.lugassy.net/.well-known/oauth-protected-resource", auth_md="https://ai-daily.lugassy.net/auth.md"
   ```

2. **Well-known metadata.** Fetch the RFC 9728 protected-resource metadata, then follow `authorization_servers` (or `authorization_server_metadata`) to the RFC 8414 authorization-server metadata. Both documents publish an `agent_auth` block with `register_uri`, `claim_uri`, `revocation_uri`, and `identity_types_supported`.

   ```bash
   curl https://ai-daily.lugassy.net/.well-known/oauth-protected-resource
   curl https://ai-daily.lugassy.net/.well-known/oauth-authorization-server
   curl https://ai-daily.lugassy.net/.well-known/openid-configuration   # mirrors agent_auth for OIDC clients
   ```

## Pick a method

Three identity flavors are advertised under `agent_auth.identity_types_supported`. Pick the one that fits your agent:

| identity_type | When to use | Endpoint |
| --- | --- | --- |
| `anonymous` | You only need to read. No auth header required at all. | (no call needed) |
| `client_credentials` | You want a per-request bearer for audit logs or quotas. | `POST https://ai-daily.lugassy.net/oauth/token` with `grant_type=client_credentials` |
| `identity_assertion` | You need an id-jag-style replayable assertion bound to a subject. | `POST https://ai-daily.lugassy.net/oauth/claim` |

All three live on the same public client (`client_id=public`, no client secret, PKCE S256 supported). No tier is rate-limited differently — the choice is about credential shape, not access level.

## Register

The `register_uri` is [`https://ai-daily.lugassy.net/oauth/register`](https://ai-daily.lugassy.net/oauth/register) — RFC 7591 Dynamic Client Registration. The endpoint is open to anyone and returns the pre-issued public client id without an out-of-band approval step. There is no email confirmation, no contact-sales gate, and no manual provisioning queue.

```bash
curl -X POST https://ai-daily.lugassy.net/oauth/register \
  -H 'Content-Type: application/json' \
  -d '{"redirect_uris":["https://your-app.example/cb"],"application_type":"native"}'

# 201 Created
# {
#   "client_id": "public",
#   "client_secret": null,
#   "token_endpoint_auth_method": "none",
#   "grant_types": ["authorization_code", "client_credentials", "refresh_token"],
#   "scope": "read:episodes read:transcripts search:episodes"
# }
```

Self-serve sandbox: production *is* the sandbox. Endpoints are read-only over static episode data, so there is no separate staging environment or test-key handoff — call the live URLs from day one.

## Claim

The `claim_uri` is [`https://ai-daily.lugassy.net/oauth/claim`](https://ai-daily.lugassy.net/oauth/claim). It mints an `identity_assertion` JWT bound to a per-request anonymous subject. Use this when you want an id-jag-shaped assertion to exchange for a bearer at another resource server, or to replay against this one.

```bash
curl -X POST https://ai-daily.lugassy.net/oauth/claim \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'identity_type=identity_assertion&scope=read%3Aepisodes%20read%3Atranscripts%20search%3Aepisodes'

# {
#   "identity_assertion": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6Ii4uLiJ9.<payload>.<sig>",
#   "token_type": "identity_assertion",
#   "identity_type": "identity_assertion",
#   "subject": "anonymous-<random>",
#   "scope": "read:episodes read:transcripts search:episodes",
#   "expires_in": 3600,
#   "replay_as_bearer": true
# }
```

**id-jag exchange (Identity Assertion Grant):** the assertion is acceptable at `/oauth/token` under the JWT-bearer grant type. This is the spec-anchor flow for agents that present a pre-issued assertion from another AS:

```bash
curl -X POST https://ai-daily.lugassy.net/oauth/token \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
  --data-urlencode 'assertion=<identity_assertion-from-claim>'
```

(For the simple anonymous case you can also call `grant_type=client_credentials` directly and skip the claim step entirely — the resulting bearer is functionally equivalent for this API.)

## Use the credential

Send the bearer (or identity_assertion replayed as a bearer) in the `Authorization` header:

```bash
curl -H 'Authorization: Bearer <token>' 'https://ai-daily.lugassy.net/api/search?q=ai'
curl -H 'Authorization: Bearer <token>' 'https://ai-daily.lugassy.net/episodes.json'

# MCP transport — same bearer works on POST /mcp:
curl -X POST https://ai-daily.lugassy.net/mcp \
  -H 'Authorization: Bearer <token>' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Tokens are JWS (EdDSA when `SIGNING_PRIVATE_KEY` is configured, HS256 otherwise). Verify against the JWKS at [`/oauth/jwks.json`](`/oauth/jwks.json`). Claims: `iss`, `sub`, `aud`, `iat`, `exp`, `scope`, `client_id`. Token TTL is one hour; refresh via the `refresh_token` grant or by calling `/oauth/token` again.

**Bearer is optional.** Every endpoint accepts unauthenticated calls — the bearer surface exists so agents that require an OAuth handshake (audit pipelines, strict MCP clients, id-jag bridges) have one.

## Errors

Auth-tier errors use the OAuth 2.0 standard error codes plus the project-wide JSON envelope (`{ error: { code, message, hint, docs_url } }`). The 401 path returns the spec-anchor `WWW-Authenticate` header with a `resource_metadata` parameter pointing at the PRM.

| Status | Code | Trigger |
| --- | --- | --- |
| 400 | `invalid_request` | Malformed token request (e.g. PKCE `code_verifier` missing). |
| 400 | `invalid_grant` | Authorization code expired or PKCE verifier mismatch. |
| 400 | `unsupported_grant_type` | `grant_type` is not one of the advertised values. |
| 400 | `unsupported_response_type` | `/oauth/authorize` only supports `code`. |
| 401 | `unauthorized` | Returned from `/agent/auth` with `WWW-Authenticate: Bearer resource_metadata=…` so callers can discover the PRM via a single probe. |
| 405 | `method_not_allowed` | Wrong HTTP verb on an OAuth endpoint. |
| 429 | `rate_limited` | Per-IP rate limit exceeded; `Retry-After` is set. |

Example 401 response from `/agent/auth`:

```
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="https://ai-daily.lugassy.net", scope="read:episodes read:transcripts search:episodes", resource_metadata="https://ai-daily.lugassy.net/.well-known/oauth-protected-resource", auth_md="https://ai-daily.lugassy.net/auth.md"
Content-Type: application/json

{
  "error": {
    "code": "unauthorized",
    "message": "Auth challenge — present a bearer token or skip auth entirely.",
    "hint": "https://ai-daily.lugassy.net/auth.md"
  }
}
```

## Revocation

The `revocation_uri` is [`https://ai-daily.lugassy.net/oauth/revoke`](https://ai-daily.lugassy.net/oauth/revoke) — RFC 7009. Tokens are stateless JWS, so revocation is a courtesy acknowledgement rather than a session lookup: the endpoint accepts any token (or none) and returns `200 OK` as the spec mandates. Tokens still expire on their own one-hour TTL.

```bash
curl -X POST https://ai-daily.lugassy.net/oauth/revoke \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'token=<access_token>&token_type_hint=access_token'

# 200 OK (empty body, per RFC 7009)
```

If you cycle keys (rotate `SIGNING_PRIVATE_KEY`), all previously-issued tokens stop verifying at the new JWK published under [`/oauth/jwks.json`](`/oauth/jwks.json`). That is the operational revocation mechanism — useful in incident response if a token leaks.

## See also

- Authorization-server metadata (RFC 8414): [`https://ai-daily.lugassy.net/.well-known/oauth-authorization-server`](https://ai-daily.lugassy.net/.well-known/oauth-authorization-server)
- Protected-resource metadata (RFC 9728): [`https://ai-daily.lugassy.net/.well-known/oauth-protected-resource`](https://ai-daily.lugassy.net/.well-known/oauth-protected-resource)
- OIDC discovery: [`https://ai-daily.lugassy.net/.well-known/openid-configuration`](https://ai-daily.lugassy.net/.well-known/openid-configuration)
- JWKS: [`https://ai-daily.lugassy.net/oauth/jwks.json`](https://ai-daily.lugassy.net/oauth/jwks.json)
- Agent integration guide: [`https://ai-daily.lugassy.net/AGENTS.md`](https://ai-daily.lugassy.net/AGENTS.md)
- Developer docs: [`https://ai-daily.lugassy.net/docs.md`](https://ai-daily.lugassy.net/docs.md)
