ATProto OAuth Integration Spec#
Relay OAuth Provider
v0.1 Draft — March 2026
Companion to: Provisioning API Spec, Mobile Architecture Spec
1. Overview#
The relay must be a compliant ATProto OAuth 2.1 authorization server so that third-party apps (Bluesky, etc.) can authenticate users and create records via XRPC. This document specifies how the relay integrates existing Rust OAuth libraries rather than building OAuth from scratch.
1.1 Why OAuth Matters#
Without a compliant OAuth provider, no third-party app can authenticate against the relay. A user who creates an identity through the mobile app or desktop PDS cannot log into Bluesky — the entire product is unusable. OAuth is on the critical path for every lifecycle phase.
1.2 ATProto OAuth Requirements#
The ATProto OAuth spec requires PDS implementations to support:
- OAuth 2.1 authorization code flow with PKCE (S256 only)
- DPoP (Demonstrating Proof-of-Possession) using ES256, with unique JTI per request and nonce support
- PAR (Pushed Authorization Requests) — mandatory for all client types
- Dynamic Client Registration (RFC 7591) — clients provide metadata URLs, not pre-registered credentials
- Server metadata at
/.well-known/oauth-authorization-server - JWKS endpoint for public key discovery
- Grant types:
authorization_codeandrefresh_token - Token endpoint auth:
noneandprivate_key_jwt - Scopes:
atprotoandtransition:generic - CORS support for browser-based apps
- Refresh tokens are single-use (rotation on each use)
- Tokens bound to DPoP key and client_id
2. Existing Rust Ecosystem#
2.1 Recommended: atproto-oauth-axum#
Crate: atproto-oauth-axum (v0.14.0, Feb 2026) Author: Nick Gerakines Status: Actively maintained, 22 releases since June 2025, ~440 downloads/month
Provides pre-built Axum handlers for:
- Authorization endpoint
- Token endpoint
- PAR endpoint
- JWKS endpoint
- Server metadata endpoint
- Client metadata resolution
- Authorization callback handling
This is the most direct integration path if the relay uses Axum (which aligns with the Rust web server ecosystem).
2.2 Alternative: atproto-oauth-aip#
Crate: atproto-oauth-aip Status: Same author, lower-level workflow library
Use this if the relay uses a different HTTP framework (e.g., actix-web) or needs more control over the OAuth flow. Provides the OAuth logic without Axum-specific bindings.
2.3 Reference Implementation: graze-social/aip#
Repo: graze-social/aip (105 stars, v2.2.3, Jan 2026) Status: Production-ready, Docker support, multiple storage backends
A complete standalone OAuth 2.1 authorization server with native ATProto integration. Useful as:
- Reference for how a production ATProto OAuth server works
- Potential deployment as a separate sidecar service (vs. embedding in the relay)
- Storage backend patterns (SQLite, PostgreSQL)
3. Integration Architecture#
3.1 Deployment Model#
Two viable approaches:
Option A: Embedded (recommended for v1.0)
The relay process embeds atproto-oauth-axum handlers directly into its Axum router. OAuth state lives in the same database as relay state. Simplest deployment — one process, one database.
[Third-party app] → HTTPS → [Relay: Axum router]
├── /oauth/* → atproto-oauth-axum handlers
├── /xrpc/* → XRPC proxy/handler
└── /v1/* → Provisioning API
Option B: Sidecar
Deploy graze-social/aip as a separate service. The relay delegates OAuth to the sidecar and validates tokens on XRPC requests. More complex but isolates OAuth concerns.
Not recommended for v1.0 — adds operational complexity for a solo developer.
3.2 Storage#
OAuth state (authorization codes, tokens, sessions, client metadata cache) stored in the relay's SQLite database. Both atproto-oauth-axum and graze-social/aip support SQLite backends.
Tables needed:
oauth_authorization_codes— short-lived, per-authorization-flowoauth_access_tokens— bound to DPoP key, client_id, accountoauth_refresh_tokens— single-use, rotated on each useoauth_client_metadata_cache— cached client metadata from discovery URLsoauth_dpop_nonces— replay prevention
3.3 Account Binding#
The OAuth provider needs to map ATProto DIDs to relay accounts. During authorization:
- User is redirected to relay's authorization endpoint
- Relay resolves the user's DID → account_id
- User authenticates (password, or session token if already logged in)
- Relay issues tokens bound to the account
The relay's existing session/authentication system (provisioning API §2) handles step 3. The OAuth library handles everything else.
4. Lifecycle Phase Behavior#
4.1 Mobile-Only Phase#
The relay is a full PDS. OAuth works identically to any hosted PDS:
- Authorization, token, and XRPC endpoints all on the relay
- Relay stores repo, signs commits, serves reads
- Third-party apps see a normal PDS
No special behavior needed. This is the standard ATProto OAuth flow.
4.2 Desktop-Enrolled Phase#
The relay is still the OAuth provider and XRPC endpoint. The difference is internal:
- Write XRPC calls (createRecord, etc.) are proxied to the desktop for repo construction before the relay signs them
- Read XRPC calls can be served from relay cache
- OAuth tokens and sessions are managed entirely at the relay — the desktop is invisible to third-party apps
No OAuth changes needed for desktop enrollment. This is the key advantage of the relay-as-permanent-endpoint architecture.
4.3 Desktop Offline (During Desktop-Enrolled Phase)#
- Read XRPC calls: served from relay cache (no change to OAuth)
- Write XRPC calls: relay returns 503 to the XRPC caller
- OAuth tokens remain valid — the 503 is at the XRPC layer, not the auth layer
Third-party apps see a PDS that accepts reads but rejects writes. This is a known ATProto pattern (PDS maintenance mode).
5. Endpoints#
The relay must serve these endpoints at its base URL (the DID document's service endpoint):
| Endpoint | Source | Purpose |
|---|---|---|
/.well-known/oauth-authorization-server |
atproto-oauth-axum | Server metadata (issuer, endpoints, supported flows) |
/oauth/authorize |
atproto-oauth-axum | Authorization endpoint (user-facing) |
/oauth/token |
atproto-oauth-axum | Token endpoint (app-facing) |
/oauth/par |
atproto-oauth-axum | Pushed Authorization Request endpoint |
/oauth/jwks |
atproto-oauth-axum | Public keys for token verification |
/oauth/callback |
atproto-oauth-axum | Authorization callback |
These are in addition to the relay's existing endpoints:
/v1/*— provisioning API/xrpc/*— ATProto XRPC
5.1 Server Metadata#
The /.well-known/oauth-authorization-server response must include:
{
"issuer": "https://relay.example.com",
"authorization_endpoint": "https://relay.example.com/oauth/authorize",
"token_endpoint": "https://relay.example.com/oauth/token",
"pushed_authorization_request_endpoint": "https://relay.example.com/oauth/par",
"jwks_uri": "https://relay.example.com/oauth/jwks",
"scopes_supported": ["atproto", "transition:generic"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none", "private_key_jwt"],
"code_challenge_methods_supported": ["S256"],
"dpop_signing_alg_values_supported": ["ES256"]
}
6. Authorization UI#
The relay needs a minimal web UI for the OAuth authorization screen. When a third-party app redirects a user to /oauth/authorize, the relay must:
- Show the app's name and permissions requested
- Allow the user to approve or deny
- Redirect back to the app with an authorization code
For v1.0, this can be a minimal server-rendered page. No SPA needed. The provisioning API's session system handles user authentication.
For BYO relay operators, the authorization UI should be customizable (branding, colors) via relay config.
7. Security Considerations#
7.1 Token Storage#
Access tokens and refresh tokens are stored server-side. The relay validates DPoP proofs on every request, preventing token theft from being useful without the DPoP private key.
7.2 Client Metadata Caching#
ATProto uses dynamic client registration — clients provide a metadata URL, not pre-registered credentials. The relay must:
- Fetch and cache client metadata on first authorization
- Re-validate periodically (TTL: 24 hours recommended)
- Reject clients with unreachable or invalid metadata
7.3 Rate Limiting#
OAuth endpoints should be rate-limited separately from XRPC and provisioning API endpoints. Recommended limits:
- Authorization: 10/min per IP
- Token: 30/min per client_id
- PAR: 30/min per client_id
7.4 BYO Relay Implications#
Self-hosted relay operators run their own OAuth provider. The BYO relay binary (Nix/Docker) must include the OAuth endpoints. The authorization UI defaults should be sensible without configuration.
8. Implementation Milestones#
v0.1 — Basic OAuth (blocks mobile-only phase)#
- Integrate
atproto-oauth-axuminto relay's Axum router - SQLite-backed token storage
- Minimal authorization UI (server-rendered)
- Server metadata endpoint
- Test with Bluesky app as client
v1.0 — Production OAuth#
- PostgreSQL storage backend option
- Client metadata caching with TTL
- Rate limiting on OAuth endpoints
- Customizable authorization UI for BYO relay operators
- Token revocation endpoint
- Audit logging of authorization grants
Later#
- Scoped tokens (read-only grants for specific collections)
- Token introspection endpoint
- Admin dashboard for managing active OAuth sessions
9. Integration Checklist#
Before the relay can accept third-party app logins:
-
/.well-known/oauth-authorization-serverreturns valid metadata -
/oauth/authorizerenders authorization UI and handles consent -
/oauth/tokenissues DPoP-bound access + refresh tokens -
/oauth/paraccepts pushed authorization requests -
/oauth/jwksreturns current signing keys - PKCE (S256) enforced on all flows
- DPoP proof validated on every token request
- Refresh token rotation (single-use) working
- Bluesky app can complete full OAuth flow
- Bluesky app can create a post via XRPC after OAuth
- Token bound to correct account/DID
10. Design Decisions#
| Decision | Rationale | Alternatives Considered |
|---|---|---|
Embed atproto-oauth-axum in relay process |
Simplest deployment for solo dev. One process, one DB. | Sidecar (graze-social/aip) — more complex ops. |
| SQLite for OAuth storage in v1.0 | Matches relay's existing storage. No additional infra. | PostgreSQL from day one — overkill for early users. |
| Minimal server-rendered auth UI | OAuth authorization screen is visited rarely. No SPA needed. | Full React SPA — unnecessary complexity. |
| Use existing crates, don't build OAuth | ATProto OAuth is complex (DPoP, PAR, PKCE, dynamic registration). Building from scratch is months of work. | Build custom — slower, more bugs, no community fixes. |