atproto-oauth
This skill should be used when the user is implementing, auditing, or debugging AT Protocol OAuth in Rust, TypeScript, or Go — covering confidential backend (BFF) clients, public SPA clients, native desktop clients, the authorization flow (PAR / DPoP / PKCE), client metadata publication, permission / scope design, refresh token handling, session storage, and server-side DPoP validation. Triggers on phrases like "OAuth client metadata", "client_id as URL", "private_key_jwt", "dpop_bound_access_tokens", "dpop_signing_alg_values_supported", "PAR", "pushed authorization request", "request_uri", "DPoP proof", "DPoP nonce", "use_dpop_nonce", "invalid_dpop_proof", "ath claim", "htu", "htm", "jkt", "jwk thumbprint", "refresh token race", "token rotation", "invalid_grant", "access_denied", "state store", "session store", "requestLock", "oauth/authorize", "oauth/callback", "oauth/token", "oauth/par", "oauth/revoke", ".well-known/oauth-protected-resource", ".well-known/oauth-authorization-server", "permission-set", "transition:generic", "transition:email", "atproto scope", "account:email", "identity:handle", "repo:*", "blob:*", "rpc:*", "include:", "iss parameter", "RFC 9449", "RFC 9126", "RFC 7636", "RFC 7523", "RFC 9207", "OAuth 2.1", "SameSite=Lax for OAuth", "confidential client", "public client", "BFF pattern", "SPA OAuth", "app password migration". Also triggers on dependency/import names like `atproto-oauth`, `@atproto/oauth-client-node`, `@atproto/oauth-client-browser`, `@atproto/oauth-client`, `@atproto/oauth-types`, `@atproto/jwk-jose`, `indigo/atproto/auth/oauth`, `NodeOAuthClient`, `BrowserOAuthClient`, `ClientApp`, `ClientSession`, `ClientAuthStore`, `OAuthClient`, `OAuthRequestStorage`, `DpopRetry`, `validate_dpop_jwt`, `auth_dpop`, `request_dpop`, `StartAuthFlow`, `ProcessCallback`, `ResumeSession`, `JoseKey`. Use this skill to build a login/callback/refresh/logout flow, publish a `/oauth-client-metadata.json` document, implement a pre-flow `state` store or post-flow session store, design permission sets, set up DPoP on resource requests, or debug token/DPoP failures. Covers identity verification (`sub` DID → DID doc → PDS → AS match), session-cookie hardening (SameSite=Lax, HttpOnly, encryption at rest), refresh-race mitigation, multi-node BFF lock patterns, SSRF hardening on metadata fetches, and cross-language differences between the three reference implementations. Does NOT cover DID / handle resolution in depth (see `atproto-identity-resolution`), CAR / MST / commit signing (see `atproto-repository`), CID parsing (see `atproto-cid`), lexicon-level record validation or XRPC method invocation beyond OAuth (see `atproto-lexicon`), or app-password flows (a separate, legacy mechanism being deprecated in favor of OAuth).
Works with
---
name: atproto-oauth
description: This skill should be used when the user is implementing, auditing, or debugging AT Protocol OAuth in Rust, TypeScript, or Go — covering confidential backend (BFF) clients, public SPA clients, native desktop clients, the authorization flow (PAR / DPoP / PKCE), client metadata publication, permission / scope design, refresh token handling, session storage, and server-side DPoP validation. Triggers on phrases like "OAuth client metadata", "client_id as URL", "private_key_jwt", "dpop_bound_access_tokens", "dpop_signing_alg_values_supported", "PAR", "pushed authorization request", "request_uri", "DPoP proof", "DPoP nonce", "use_dpop_nonce", "invalid_dpop_proof", "ath claim", "htu", "htm", "jkt", "jwk thumbprint", "refresh token race", "token rotation", "invalid_grant", "access_denied", "state store", "session store", "requestLock", "oauth/authorize", "oauth/callback", "oauth/token", "oauth/par", "oauth/revoke", ".well-known/oauth-protected-resource", ".well-known/oauth-authorization-server", "permission-set", "transition:generic", "transition:email", "atproto scope", "account:email", "identity:handle", "repo:*", "blob:*", "rpc:*", "include:", "iss parameter", "RFC 9449", "RFC 9126", "RFC 7636", "RFC 7523", "RFC 9207", "OAuth 2.1", "SameSite=Lax for OAuth", "confidential client", "public client", "BFF pattern", "SPA OAuth", "app password migration". Also triggers on dependency/import names like `atproto-oauth`, `@atproto/oauth-client-node`, `@atproto/oauth-client-browser`, `@atproto/oauth-client`, `@atproto/oauth-types`, `@atproto/jwk-jose`, `indigo/atproto/auth/oauth`, `NodeOAuthClient`, `BrowserOAuthClient`, `ClientApp`, `ClientSession`, `ClientAuthStore`, `OAuthClient`, `OAuthRequestStorage`, `DpopRetry`, `validate_dpop_jwt`, `auth_dpop`, `request_dpop`, `StartAuthFlow`, `ProcessCallback`, `ResumeSession`, `JoseKey`. Use this skill to build a login/callback/refresh/logout flow, publish a `/oauth-client-metadata.json` document, implement a pre-flow `state` store or post-flow session store, design permission sets, set up DPoP on resource requests, or debug token/DPoP failures. Covers identity verification (`sub` DID → DID doc → PDS → AS match), session-cookie hardening (SameSite=Lax, HttpOnly, encryption at rest), refresh-race mitigation, multi-node BFF lock patterns, SSRF hardening on metadata fetches, and cross-language differences between the three reference implementations. Does NOT cover DID / handle resolution in depth (see `atproto-identity-resolution`), CAR / MST / commit signing (see `atproto-repository`), CID parsing (see `atproto-cid`), lexicon-level record validation or XRPC method invocation beyond OAuth (see `atproto-lexicon`), or app-password flows (a separate, legacy mechanism being deprecated in favor of OAuth).
license: MIT
---
# AT Protocol OAuth
AT Protocol OAuth is an **OAuth 2.1** profile with mandatory **PKCE (S256)**, **PAR**, **DPoP**, and URL-based dynamic client registration via a published **client metadata document**. No `client_secret` — confidential clients authenticate to the token endpoint with a `private_key_jwt` assertion (ES256). Public / SPA / native clients authenticate by DPoP proof alone.
This skill routes to per-language guides for Rust, TypeScript, and Go, sitting on top of a language-neutral spec in `shared/`.
## Defaults
- **`client_id` is a URL.** It resolves to a JSON metadata document the AS fetches on demand. The URL path, host, and protocol must match byte-for-byte between registration, PAR, and authorize.
- **Every access token is DPoP-bound.** `dpop_bound_access_tokens: true` is required in client metadata; every resource request carries a fresh DPoP proof with `ath = SHA-256(access_token)` and a per-origin `nonce`.
- **PAR is required.** You push the authorize request to `pushed_authorization_request_endpoint` and redirect the user to `{AS}/oauth/authorize?client_id=...&request_uri=urn:ietf:params:oauth:request_uri:...`. Query parameters never hit the user-agent.
- **Scopes start with `atproto`.** All flows must request `atproto` as the first scope. Further scopes are layered on: `transition:generic`, `account:email?action=read`, `rpc:app.bsky.feed.*`, `include:<permission-set>`, etc.
- **The session belongs to the DID.** `sub` in the token response is a DID. Handles may change; DIDs don't. Persist by DID.
- **Identity verification is mandatory**: `sub` → DID document → `#atproto_pds` → matches the PDS you discovered → `authorization_servers[0]` → matches the AS you talked to. Skip this step = CSRF window.
Full normative rules: `shared/spec.md`, `shared/flows.md`, `shared/client-metadata.md`, `shared/dpop.md`, `shared/scopes.md`, `shared/sessions.md`, `shared/security-requirements.md`. Fixtures: `shared/test-vectors.md`. Common failures: `shared/troubleshooting.md`. Cross-language differences: `shared/divergence-matrix.md`.
## Language detection
Before generating or reviewing any OAuth code, determine the target language from project files or the file being edited:
- `Cargo.toml`, `*.rs`, mention of `atproto-oauth` / `atproto-identity` / `atproto-oauth-aip` → **Rust** — read from `rust/`.
- `package.json`, `tsconfig.json`, `*.ts`, `*.tsx`, imports of `@atproto/oauth-client-node` / `@atproto/oauth-client-browser` / `@atproto/oauth-client` / `@atproto/jwk-jose` → **TypeScript** — read from `typescript/`. Also `*.js`/`*.jsx` when there's no `.ts`.
- `go.mod`, `*.go`, imports of `github.com/bluesky-social/indigo/atproto/auth/oauth` → **Go** — read from `go/`.
Prefer the *file being edited* over the *repo root* when they disagree.
If multiple languages are present and the task doesn't point at one unambiguously, **ask which one applies**. Never mix OAuth libraries across languages in generated code.
If an unsupported language is detected (Python, Java, Swift, Kotlin, …), point the user at `shared/spec.md`, `shared/flows.md`, and `shared/dpop.md` for the wire format, and offer the TypeScript `@atproto/oauth-client-node` source as the most complete reference implementation to transliterate from.
## Client-type detection
Before picking a file, also determine what **kind** of client is being built:
| Kind | Clue | Route to… |
| -------------------- | --------------------------------------------------- | -------------------------------------------------------- |
| Confidential (BFF) | Has server-side Rust/TS/Go code + a signing key | Node: `typescript/*.md` with `NodeOAuthClient`. Rust/Go: only option. |
| Public (SPA) | Browser-only; no server; tokens land in the browser | **TypeScript only** → `typescript/*.md` with `BrowserOAuthClient`. Rust and Go don't ship a browser client. |
| Public (native) | Desktop / mobile app with custom-scheme redirects | TypeScript (`NodeOAuthClient` with `token_endpoint_auth_method: none`). Rust can do it manually. Go: unsupported. |
| AS or resource server | Implementing the server side (rare) | Rust only (`rust/dpop.md` §server-side). TS and Go don't ship validators. |
When in doubt, **default to confidential BFF** — it's the recommended pattern for any app that has a backend, and it keeps secrets out of the browser.
## Reading guide
For every OAuth task:
1. Read the relevant `shared/*.md` first. They define the rules your code must enforce. Usually `shared/spec.md` + one of `shared/flows.md` / `shared/dpop.md` / `shared/sessions.md` / `shared/client-metadata.md` / `shared/scopes.md`.
2. Read the relevant task file in the detected language directory:
- Publishing `/oauth-client-metadata.json` + `/jwks.json` → `{lang}/client-metadata.md`
- Authorize / callback / refresh / logout flow → `{lang}/flows.md`
- DPoP minting + nonce retry + server-side validation → `{lang}/dpop.md`
- Pre-flow state, sessions, refresh-race mitigation → `{lang}/sessions.md`
- Library setup, public API, idioms → `{lang}/README.md`
3. Consult `shared/divergence-matrix.md` whenever porting between languages or reviewing cross-stack interop.
4. Consult `shared/troubleshooting.md` when debugging a specific failure (`invalid_dpop_proof`, `invalid_grant`, callback cookie missing, etc.).
5. Before publishing a metadata doc, run `scripts/validate_client_metadata.py` against the served URL.
Always prefer the official library over hand-rolling: `atproto-oauth` in Rust, `@atproto/oauth-client-*` in TypeScript, `indigo/atproto/auth/oauth` in Go. The protocol is small but unforgiving — every byte of the wire matters.
## The conceptual stack
```
┌─────────────────────────────────────────────┐
│ Client metadata document (JSON at client_id)│ ← published by you
│ { client_id, redirect_uris, jwks_uri, … } │
└─────────────────────────────────────────────┘
│ fetched once by AS
▼
┌─────────────────────────────────────────────┐
│ PAR → /oauth/par │ ← flow starts
│ POST client_assertion + DPoP + PKCE + scope │
│ ↓ │
│ request_uri: urn:ietf:params:oauth:request_uri:…│
└─────────────────────────────────────────────┘
│ redirect user agent to authorize
▼
┌─────────────────────────────────────────────┐
│ User logs in at AS's UI, grants consent │
│ ↓ │
│ Redirect back: ?code=…&state=…&iss=… │
└─────────────────────────────────────────────┘
│ callback
▼
┌─────────────────────────────────────────────┐
│ /oauth/token (code exchange) │
│ POST code + PKCE verifier + assertion + DPoP │
│ ↓ │
│ { access_token, refresh_token, sub: did, │
│ aud: pds_url, expires_in: 3600 } │
└─────────────────────────────────────────────┘
│ persist session by DID
▼
┌─────────────────────────────────────────────┐
│ Resource request to PDS │
│ Authorization: DPoP <access_token> │
│ DPoP: <proof with ath + nonce> │
└─────────────────────────────────────────────┘
```
## Cross-language hazards to flag up front
High-frequency failure modes; full detail in `shared/divergence-matrix.md`:
- **Refresh race** — Two concurrent requests both refresh, one invalidates the other's refresh token, dead session. TS has `NodeRequestLock` built in; Rust and Go leave it to the caller. Every production BFF needs a per-DID lock (Redis/Postgres advisory or in-process mutex).
- **`htu` normalization** — Query strings and fragments must be stripped before minting a DPoP proof; default ports must be elided. TS does this automatically; Rust and Go don't. `invalid_dpop_proof` with identical-looking URLs = suspect this.
- **Rust's hard-coded `ES256` header** — `auth_dpop()` writes `alg: ES256` into the JWT header even when the key is P-384. Non-P-256 keys need `dpop::mint` with a custom header.
- **Rust leaks private JWK components** — `jwk::generate()` on a private `KeyData` serializes the `d` field unless the caller runs `to_public(&key)` first. TS and Go strip automatically.
- **Go is BFF-only** — No SPA support. No native client support in `indigo`. If the task is browser OAuth, route to TypeScript regardless of repo language.
- **`SameSite=Strict` kills the callback** — OAuth redirects are cross-origin top-level navigations; `Strict` drops the cookie and the callback handler can't find pre-flow state. Always `Lax` on session cookies.
- **Public clients have a 14-day refresh cap, not 180.** Silent until day 15 when `invalid_grant` suddenly starts failing.
## Tools you can call
Prefer these MCP tools when the goal is to validate or compute rather than teach an implementation how:
- **`lexicon-garden`** → `discover_permission_sets`, `check_compatibility` (scope tooling), `describe_me` (authorize the session).
- **`atpmcp`** → `resolve_handle_to_did`, `resolve_identity` (identity lookups — needed for PDS/AS discovery).
For scope authoring and permission-set design, the normative source is <https://atproto.com/specs/permission> and <https://atproto.com/guides/permission-sets>.
## Validator script
`scripts/validate_client_metadata.py` checks a served client metadata document for the invariants in `shared/test-vectors.md` §V5. Run it in CI against your deployed URL:
```
$ python scripts/validate_client_metadata.py https://app.example.com/oauth-client-metadata.json
```
Catches mutations like missing `dpop_bound_access_tokens`, wrong `token_endpoint_auth_signing_alg`, inline `jwks` containing private `d` field, `http://` redirect outside loopback, etc.
Exit codes: `0` = document passes every invariant; `1` = at least one invariant failed (reasons printed to stdout); `2` = usage error (missing URL, network failure, non-JSON response). The script is stdlib-only and makes a single HTTP GET — no runtime dependencies, safe for CI sandboxes.
## Directory layout
```
atproto-oauth/
├── SKILL.md # this file — router
├── scripts/
│ └── validate_client_metadata.py # CI validator for metadata doc
├── shared/
│ ├── spec.md # OAuth 2.1 + AT Proto profile: entities, invariants
│ ├── flows.md # byte-level wire content for each step
│ ├── client-metadata.md # metadata document fields, JWKS rules
│ ├── dpop.md # RFC 9449 profile
│ ├── scopes.md # scope grammar, permission sets
│ ├── sessions.md # pre-flow state + post-flow session rules
│ ├── security-requirements.md # cookies, keys, tokens, SSRF
│ ├── troubleshooting.md # common failures and diagnosis
│ ├── test-vectors.md # fixtures for conformance
│ └── divergence-matrix.md # cross-language differences
├── rust/
│ ├── README.md # atproto-oauth setup
│ ├── client-metadata.md # jwk::generate + Axum handlers
│ ├── flows.md # oauth_init / oauth_complete / oauth_refresh
│ ├── dpop.md # auth_dpop / request_dpop / DpopRetry / validate_dpop_jwt
│ └── sessions.md # OAuthRequestStorage + custom session abstraction + refresh race
├── typescript/
│ ├── README.md # @atproto/oauth-client-* setup
│ ├── client-metadata.md # client.clientMetadata + client.jwks + JoseKey
│ ├── flows.md # authorize / callback / restore / revoke + BrowserOAuthClient
│ ├── dpop.md # invisible fetchHandler DPoP + per-origin nonce cache
│ └── sessions.md # StateStore / SessionStore / NodeRequestLock + IndexedDB SPA
└── go/
├── README.md # indigo/atproto/auth/oauth setup
├── client-metadata.md # cfg.ClientMetadata() + cfg.PublicJWKS()
├── flows.md # StartAuthFlow / ProcessCallback / ResumeSession / Logout
├── dpop.md # NewAuthDPoP + automatic ClientSession DPoP
└── sessions.md # ClientAuthStore + refresh race (caller-owned)
```
## References
All reachable from the tree above. Listed here for quick grep:
- `shared/spec.md`, `shared/flows.md`, `shared/client-metadata.md`, `shared/dpop.md`, `shared/scopes.md`, `shared/sessions.md`, `shared/security-requirements.md`, `shared/troubleshooting.md`, `shared/test-vectors.md`, `shared/divergence-matrix.md`
- `rust/README.md`, `rust/client-metadata.md`, `rust/flows.md`, `rust/dpop.md`, `rust/sessions.md`
- `typescript/README.md`, `typescript/client-metadata.md`, `typescript/flows.md`, `typescript/dpop.md`, `typescript/sessions.md`
- `go/README.md`, `go/client-metadata.md`, `go/flows.md`, `go/dpop.md`, `go/sessions.md`
Upstream normative sources:
- <https://atproto.com/specs/oauth> — AT Proto OAuth profile
- <https://atproto.com/specs/permission> — scopes and permission sets
- <https://atproto.com/guides/auth>, <https://atproto.com/guides/about-oauth>, <https://atproto.com/guides/oauth-patterns>, <https://atproto.com/guides/sdk-auth>, <https://atproto.com/guides/permission-requests>, <https://atproto.com/guides/permission-sets> — conceptual guides
- RFC 9449 (DPoP), RFC 7636 (PKCE), RFC 9126 (PAR), RFC 7523 (JWT client auth), RFC 8414 (server metadata), RFC 9207 (`iss`), OAuth 2.1 draftMore Debugging skills
diagnosing-bugs
mattpocock/skills
Diagnosis loop for hard bugs and performance regressions. Use when the user says "diagnose"/"debug this", or reports something broken/throwing/failing/slow.
explore-code
lllllllama/rigorpilot-skills
Rigor Improve implementation leaf skill for auditable candidate implementation in deep learning research repositories. Use when the researcher explicitly authorizes exploratory work on an isolated branch or worktree to transplant modules, adapt a backbone, add LoRA or adapter layers, replace a head, or stitch together meaningful low-risk migration ideas with rollback-aware records in `explore_outputs/`. Do not use for end-to-end exploration orchestration on top of `current_research`, trusted baseline reproduction, conservative debugging, environment setup, verified contribution claims, or default repository analysis.
safe-debug
lllllllama/rigorpilot-skills
Rigor Debug / Rigor Audit skill for deep learning research work. Use when the user pastes a traceback, terminal error, CUDA OOM, checkpoint load failure, shape mismatch, NaN loss symptom, or training failure and wants conservative diagnosis before any patching, with debug fixes clearly separated from research contributions. Do not use for broad refactoring, speculative adaptation, automatic exploratory patching, or general repository familiarization.

