# OAuth Implementation in ATCR This document describes ATCR's OAuth implementation, which uses the ATProto OAuth specification with DPoP (Demonstrating Proof of Possession) for secure authentication. ## Overview ATCR implements a full OAuth 2.0 + DPoP flow following the ATProto specification. The implementation uses the [indigo OAuth library](https://github.com/bluesky-social/indigo) and extends it with ATCR-specific configuration for registry operations. ### Key Features - **DPoP (RFC 9449)**: Cryptographic proof-of-possession binds tokens to specific client keys - **PAR (RFC 9126)**: Pushed Authorization Requests for secure server-to-server parameter exchange - **PKCE (RFC 7636)**: Proof Key for Code Exchange prevents authorization code interception - **Confidential Clients**: Production deployments use P-256 private keys for client authentication - **Public Clients**: Development (localhost) uses simpler public client configuration ## Client Types ATCR supports two OAuth client types depending on the deployment environment: ### Public Clients (Development) **When:** `baseURL` contains `localhost` or `127.0.0.1` **Configuration:** - Client ID: `http://localhost?redirect_uri=...&scope=...` (query-based) - No client authentication - Uses indigo's `NewLocalhostConfig()` helper - DPoP still required for token requests **Example:** ```go // Automatically uses public client for localhost config := oauth.NewClientConfigWithScopes("http://127.0.0.1:5000", scopes) ``` ### Confidential Clients (Production) **When:** `baseURL` is a public domain (not localhost) **Configuration:** - Client ID: `{baseURL}/client-metadata.json` (metadata endpoint) - Client authentication: P-256 (ES256) private key JWT assertion - Private key stored at `/var/lib/atcr/oauth/client.key` - Auto-generated on first run with 0600 permissions - Upgraded via `config.SetClientSecret(privateKey, keyID)` **Example:** ```go // 1. Create base config (public) config := oauth.NewClientConfigWithScopes("https://atcr.io", scopes) // 2. Load or generate P-256 key privateKey, err := oauth.GenerateOrLoadClientKey("/var/lib/atcr/oauth/client.key") // 3. Generate key ID keyID, err := oauth.GenerateKeyID(privateKey) // 4. Upgrade to confidential err = config.SetClientSecret(privateKey, keyID) ``` ## Key Management ### P-256 Key Generation ATCR uses **P-256 (NIST P-256, ES256)** keys for OAuth client authentication. This differs from the K-256 keys used for ATProto PDS signing. **Why P-256?** - Standard OAuth/OIDC key algorithm - Widely supported by authorization servers - Compatible with indigo's `SetClientSecret()` API **Key Storage:** - Default path: `/var/lib/atcr/oauth/client.key` - Configurable via: `ATCR_OAUTH_KEY_PATH` environment variable - File permissions: `0600` (owner read/write only) - Directory permissions: `0700` (owner access only) - Format: Raw binary bytes (not PEM) **Key Lifecycle:** 1. On first production startup, AppView checks for key at configured path 2. If missing, generates new P-256 key using `atcrypto.GeneratePrivateKeyP256()` 3. Saves raw key bytes to disk with restrictive permissions 4. Logs generation event: `"Generated new P-256 OAuth client key"` 5. On subsequent startups, loads existing key 6. Logs load event: `"Loaded existing P-256 OAuth client key"` **Key Rotation:** To rotate the OAuth client key: 1. Stop the AppView service 2. Delete or rename the existing key file 3. Restart AppView (new key will be generated automatically) 4. Note: Active OAuth sessions may need re-authentication ### Key ID Generation The key ID is derived from the public key for stable identification: ```go func GenerateKeyID(privateKey *atcrypto.PrivateKeyP256) (string, error) { pubKey, _ := privateKey.PublicKey() pubKeyBytes := pubKey.Bytes() hash := sha256.Sum256(pubKeyBytes) return hex.EncodeToString(hash[:])[:8], nil } ``` This generates an 8-character hex ID from the SHA-256 hash of the public key. ## Authentication Flow ### AppView OAuth Flow ```mermaid sequenceDiagram participant User participant Browser participant AppView participant PDS User->>Browser: docker push atcr.io/alice/myapp Browser->>AppView: Credential helper redirects AppView->>PDS: Resolve handle → DID AppView->>PDS: Discover OAuth metadata AppView->>PDS: PAR request (with DPoP) PDS-->>AppView: request_uri AppView->>Browser: Redirect to authorization page Browser->>PDS: User authorizes PDS->>AppView: Authorization code AppView->>PDS: Token exchange (with DPoP) PDS-->>AppView: OAuth tokens + DPoP binding AppView->>User: Issue registry JWT ``` ### Key Steps 1. **Identity Resolution** - AppView resolves handle to DID via `.well-known/atproto-did` - Resolves DID to PDS endpoint via DID document 2. **OAuth Discovery** - Fetches `/.well-known/oauth-authorization-server` from PDS - Extracts `authorization_endpoint`, `token_endpoint`, etc. 3. **Pushed Authorization Request (PAR)** - AppView sends authorization parameters to PDS token endpoint - Includes DPoP header with proof JWT - Receives `request_uri` for authorization 4. **User Authorization** - User is redirected to PDS authorization page - User approves application access - PDS redirects back with authorization code 5. **Token Exchange** - AppView exchanges code for tokens at PDS token endpoint - Includes DPoP header with proof JWT - Receives access token, refresh token (both DPoP-bound) 6. **Token Storage** - AppView stores OAuth session in SQLite database - Indigo library manages token refresh automatically - DPoP key stored with session for future requests 7. **Registry JWT Issuance** - AppView validates OAuth session - Issues short-lived registry JWT (15 minutes) - JWT contains validated DID from PDS session ## DPoP Implementation ### What is DPoP? DPoP (Demonstrating Proof of Possession) binds OAuth tokens to a specific client key, preventing token theft and replay attacks. **How it works:** 1. Client generates ephemeral key pair (or uses persistent key) 2. Client includes DPoP proof JWT in Authorization header 3. Proof JWT contains hash of HTTP request details 4. Authorization server validates proof and issues DPoP-bound token 5. Token can only be used with the same client key ### DPoP Headers Every request to the PDS token endpoint includes a DPoP header: ```http POST /oauth/token HTTP/1.1 Host: pds.example.com Content-Type: application/x-www-form-urlencoded DPoP: eyJhbGciOiJFUzI1NiIsInR5cCI6ImRwb3Arand0IiwiandrIjp7Imt0eSI6Ik... grant_type=authorization_code&code=...&redirect_uri=... ``` The DPoP header is a signed JWT containing: - `htm`: HTTP method (e.g., "POST") - `htu`: HTTP URI (e.g., "https://pds.example.com/oauth/token") - `jti`: Unique request identifier - `iat`: Timestamp - `jwk`: Public key (JWK format) ### Indigo DPoP Management ATCR uses indigo's built-in DPoP management: ```go // Indigo automatically handles DPoP clientApp := oauth.NewClientApp(&config, store) // All token requests include DPoP automatically tokens, err := clientApp.ProcessCallback(ctx, params) // Refresh automatically includes DPoP session, err := clientApp.ResumeSession(ctx, did, sessionID) ``` Indigo manages: - DPoP key generation and storage - DPoP proof JWT creation - DPoP header inclusion in token requests - Token binding to DPoP keys ## Client Configuration ### Environment Variables **ATCR_OAUTH_KEY_PATH** - Path to OAuth client P-256 signing key - Default: `/var/lib/atcr/oauth/client.key` - Auto-generated on first run (production only) - Format: Raw binary P-256 private key **ATCR_BASE_URL** - Public URL of AppView service - Required for OAuth redirect URIs - Example: `https://atcr.io` - Determines client type (public vs confidential) **ATCR_UI_DATABASE_PATH** - Path to SQLite database (includes OAuth session storage) - Default: `/var/lib/atcr/ui.db` ### Client Metadata Endpoint Production deployments serve OAuth client metadata at `{baseURL}/client-metadata.json`: ```json { "client_id": "https://atcr.io/client-metadata.json", "client_name": "ATCR Registry", "client_uri": "https://atcr.io", "redirect_uris": ["https://atcr.io/auth/oauth/callback"], "scope": "atproto blob:... repo:...", "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"], "token_endpoint_auth_method": "private_key_jwt", "token_endpoint_auth_signing_alg": "ES256", "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "kid": "abc12345" } ] } } ``` For localhost, the client ID is query-based and no metadata endpoint is used. ## Scope Management ATCR requests the following OAuth scopes: **Base scopes:** - `atproto`: Basic ATProto access **Blob scopes (for layer/manifest media types):** - `blob:application/vnd.oci.image.manifest.v1+json` - `blob:application/vnd.docker.distribution.manifest.v2+json` - `blob:application/vnd.oci.image.index.v1+json` - `blob:application/vnd.docker.distribution.manifest.list.v2+json` - `blob:application/vnd.cncf.oras.artifact.manifest.v1+json` **Repo scopes (for ATProto collections):** - `repo:io.atcr.manifest`: Manifest records - `repo:io.atcr.tag`: Tag records - `repo:io.atcr.star`: Star records - `repo:io.atcr.sailor.profile`: User profile records **RPC scope:** - `rpc:com.atproto.repo.getRecord?aud=*`: Read access to any user's records Scopes are automatically invalidated on startup if they change, forcing users to re-authenticate. ## Security Considerations ### Token Security **OAuth Tokens (managed by AppView):** - Stored in SQLite database - DPoP-bound (cannot be used without client key) - Automatically refreshed by indigo library - Used for PDS API requests (manifests, service tokens) **Registry JWTs (issued to Docker clients):** - Short-lived (15 minutes) - Signed by AppView's JWT signing key - Contain validated DID from OAuth session - Used for OCI Distribution API requests ### Attack Prevention **Token Theft:** - DPoP prevents stolen tokens from being used - Tokens are bound to specific client key - Attacker would need both token AND private key **Client Impersonation:** - Confidential clients use private key JWT assertion - Prevents attackers from impersonating AppView - Public keys published in client metadata JWKS **Man-in-the-Middle:** - All OAuth flows use HTTPS in production - DPoP includes HTTP method and URI in proof - Prevents replay attacks on different endpoints **Authorization Code Interception:** - PKCE prevents code interception attacks - Code verifier required to exchange code for token - Protects against malicious redirect URI attacks ## Troubleshooting ### Common Issues **"Failed to initialize OAuth client key"** - Check that `/var/lib/atcr/oauth/` directory exists and is writable - Verify directory permissions are 0700 - Check disk space **"OAuth session not found"** - User needs to re-authenticate (session expired or invalidated) - Check that UI database is accessible - Verify OAuth session storage is working **"Invalid DPoP proof"** - Clock skew between AppView and PDS - DPoP key mismatch (token was issued with different key) - Check that indigo library is managing DPoP correctly **"Client authentication failed"** - Confidential client key may be corrupted - Key ID may not match public key - Try rotating the client key (delete and regenerate) ### Debugging Enable debug logging to see OAuth flow details: ```bash export ATCR_LOG_LEVEL=debug ./bin/atcr-appview serve ``` Look for log messages: - `"Generated new P-256 OAuth client key"` - Key was auto-generated - `"Loaded existing P-256 OAuth client key"` - Key was loaded from disk - `"Configured confidential OAuth client"` - Production confidential client active - `"Localhost detected - using public OAuth client"` - Development public client active ### Testing OAuth Flow Test OAuth flow manually: ```bash # 1. Start AppView in debug mode ATCR_LOG_LEVEL=debug ./bin/atcr-appview serve # 2. Try docker login docker login atcr.io # 3. Check logs for OAuth flow details # Look for: PAR request, token exchange, DPoP headers, etc. ``` ## References - [ATProto OAuth Specification](https://atproto.com/specs/oauth) - [RFC 9449: OAuth 2.0 Demonstrating Proof of Possession (DPoP)](https://datatracker.ietf.org/doc/html/rfc9449) - [RFC 9126: OAuth 2.0 Pushed Authorization Requests (PAR)](https://datatracker.ietf.org/doc/html/rfc9126) - [RFC 7636: Proof Key for Code Exchange (PKCE)](https://datatracker.ietf.org/doc/html/rfc7636) - [Indigo OAuth Library](https://github.com/bluesky-social/indigo/tree/main/atproto/auth/oauth)