Select the types of activity you want to include in your feed.
Stateless auth proxy that converts AT Protocol native apps from public to confidential OAuth clients. Deploy once, get 180-day refresh tokens instead of 24-hour ones.
···44444545| Variable | Required | Default | Description |
4646|----------|----------|---------|-------------|
4747-| `AUTH_PRIVATE_KEY` | Yes | — | PEM-encoded EC P-256 private key |
4747+| `AUTH_PRIVATE_KEY` | Yes | — | PEM-encoded EC P-256 private key (active signing key) |
4848| `AUTH_CLIENT_ID` | Yes | — | Your app's OAuth client_id (client-metadata.json URL) |
4949-| `AUTH_KEY_ID` | No | `atproto-auth-1` | JWKS key identifier (`kid`) |
4949+| `AUTH_KEY_ID` | No | `atproto-auth-1` | JWKS key identifier (`kid`) for the active key |
5050+| `AUTH_OLD_PRIVATE_KEY` | No | — | PEM-encoded EC P-256 private key (previous key, for rotation) |
5151+| `AUTH_OLD_KEY_ID` | No | — | JWKS key identifier (`kid`) for the old key (required with `AUTH_OLD_PRIVATE_KEY`) |
5052| `AUTH_BIND` | No | `:8080` | Listen address |
5153| `AUTH_ALLOWED_ORIGINS` | No | `*` | CORS allowed origins |
5454+| `AUTH_RATE_LIMIT_PER_IP` | No | `10` | Max requests per IP per minute on `/oauth/token` and `/oauth/par` (0 to disable) |
5555+| `AUTH_RATE_LIMIT_GLOBAL` | No | `100` | Max total requests per minute on `/oauth/token` and `/oauth/par` (0 to disable) |
52565357## How It Works
5458···116120117121## Key Rotation
118122123123+The proxy supports zero-downtime key rotation. During rotation, both old and new public keys are published in the JWKS so existing sessions bound to the old key continue to work.
124124+1191251. Generate a new key pair with a new `kid` (e.g., `atproto-auth-2`)
120120-2. Temporarily serve both old and new public keys in the JWKS
121121-3. Deploy — the auth server will fetch the updated JWKS
122122-4. After 24+ hours, remove the old key
123123-5. Update `AUTH_PRIVATE_KEY` and `AUTH_KEY_ID` to the new key only
126126+2. Set `AUTH_OLD_PRIVATE_KEY` and `AUTH_OLD_KEY_ID` to your current key values
127127+3. Set `AUTH_PRIVATE_KEY` and `AUTH_KEY_ID` to the new key
128128+4. Deploy — the JWKS now serves both keys; new assertions use the new key
129129+5. After 24+ hours, remove `AUTH_OLD_PRIVATE_KEY` and `AUTH_OLD_KEY_ID`
130130+131131+The active key (`AUTH_PRIVATE_KEY`) is always used for signing new assertions. The old key is only published in the JWKS so auth servers can still verify existing sessions.
124132125133## Security Considerations
126134···130138- **No token logging**: Token values, auth codes, and refresh tokens are never logged
131139- **HTTPS required**: The proxy must be served over HTTPS in production (handled automatically by Railway/Fly.io)
132140- **DPoP passthrough**: The proxy never sees DPoP private keys — proofs are between the device and auth server
141141+- **Rate limiting**: Per-IP and global rate limits on `/oauth/token` and `/oauth/par` (configurable, defaults to 10/min per IP, 100/min global)
133142- **Stateless**: No database, no user data stored — the only secret is the client signing key in an environment variable
134143135144## License