# ATCR OAuth Implementation ## Overview ATCR now supports ATProto OAuth authentication via Docker credential helpers. This allows users to authenticate with their ATProto identity (Bluesky account) and use Docker push/pull commands seamlessly. ## Architecture ### Components 1. **OAuth Client** (`pkg/auth/oauth/`) - Full ATProto OAuth implementation with DPoP support - Uses `authelia.com/client/oauth2` for OAuth + PAR - Uses `github.com/AxisCommunications/go-dpop` for DPoP proof generation - Automatic authorization server discovery - PKCE support for security 2. **Credential Helper** (`cmd/credential-helper/`) - Standalone binary: `docker-credential-atcr` - Implements Docker credential helper protocol - Manages OAuth flow with browser - Stores tokens securely in `~/.atcr/oauth-token.json` 3. **Registry Integration** - `/auth/exchange` endpoint exchanges OAuth tokens for registry JWTs - Existing `/auth/token` endpoint for standard Docker auth ## Dependencies - `authelia.com/client/oauth2` - OAuth client with PAR support (2⭐, Authelia-backed) - `github.com/AxisCommunications/go-dpop` - DPoP implementation (10⭐, RFC 9449 compliant) - `github.com/golang-jwt/jwt/v5` - JWT library (transitive, 11k+⭐) ## Usage ### Setup 1. Build the credential helper: ```bash go build -o docker-credential-atcr ./cmd/credential-helper ``` 2. Install it in your PATH: ```bash sudo mv docker-credential-atcr /usr/local/bin/ ``` 3. Configure Docker to use it by editing `~/.docker/config.json`: ```json { "credsStore": "atcr" } ``` ### Configuration Run the OAuth flow: ```bash docker-credential-atcr configure ``` This will: 1. Prompt for your ATProto handle (e.g., `alice.bsky.social`) 2. Open your browser for OAuth authorization 3. Store the OAuth token and DPoP key in `~/.atcr/oauth-token.json` ### Using with Docker Once configured, use Docker normally: ```bash # Push an image docker push atcr.io/alice/myapp:latest # Pull an image docker pull atcr.io/alice/myapp:latest ``` The credential helper automatically: 1. Loads your stored OAuth token 2. Refreshes it if expired 3. Exchanges it for a registry JWT 4. Provides the JWT to Docker ## How It Works ### OAuth Flow 1. **User runs** `docker-credential-atcr configure` 2. **Resolve identity**: alice.bsky.social → DID → PDS endpoint 3. **Discover auth server**: GET `{pds}/.well-known/oauth-authorization-server` 4. **Generate DPoP key**: ECDSA P-256 key pair 5. **PAR request**: POST to PAR endpoint with DPoP header + PKCE challenge 6. **Open browser**: User authorizes on their PDS 7. **Receive code**: Callback to `localhost:8888/callback` 8. **Exchange code**: POST to token endpoint with DPoP header + PKCE verifier 9. **Save tokens**: Store OAuth token + DPoP key + DID/handle ### Docker Push/Pull Flow 1. **Docker needs credentials** for `atcr.io` 2. **Calls credential helper**: `docker-credential-atcr get` 3. **Helper loads token** from `~/.atcr/oauth-token.json` 4. **Refresh if needed**: Uses refresh token + DPoP if expired 5. **Exchange for registry JWT**: POST to `/auth/exchange` with OAuth token + handle 6. **Registry validates token**: Calls `getSession` on PDS to validate token 7. **Registry issues JWT**: Creates registry JWT with validated DID/handle 8. **Return to Docker**: `{"Username": "oauth2", "Secret": ""}` 9. **Docker uses JWT**: For authentication to registry API ## Security ### DPoP (Demonstrating Proof-of-Possession) Every OAuth request includes a DPoP proof: - Unique JWT signed with ECDSA private key - Contains HTTP method, URL, timestamp, nonce - Public key (JWK) included in JWT header - Binds the token to the specific client ### PKCE (Proof Key for Code Exchange) - Code verifier generated locally - Code challenge sent in authorization request - Verifier sent in token exchange - Prevents authorization code interception ### Token Storage - Tokens stored in `~/.atcr/oauth-token.json` - File permissions: 0600 (owner read/write only) - DPoP key stored in PEM format - Refresh tokens for long-term access ## Implementation Details ### Code Structure ``` pkg/auth/oauth/ ├── client.go # OAuth client with DPoP ├── discovery.go # Authorization server discovery ├── metadata.go # Client metadata document ├── storage.go # Token persistence └── transport.go # DPoP HTTP transport pkg/auth/atproto/ ├── session.go # ATProto session validation (Basic auth) └── validator.go # OAuth token validation via getSession cmd/credential-helper/ ├── main.go # Docker credential helper protocol ├── oauth.go # OAuth flow orchestration └── token.go # Token management pkg/auth/exchange/ └── handler.go # OAuth → Registry JWT exchange ``` ### Key Classes **OAuth Client** (`pkg/auth/oauth/client.go`) - `NewClient()` - Create client with DPoP key - `InitializeForHandle()` - Discover auth server - `AuthorizeURL()` - Generate authorization URL with PAR + PKCE - `Exchange()` - Exchange code for token with DPoP - `RefreshToken()` - Refresh expired token with DPoP **DPoP Transport** (`pkg/auth/oauth/transport.go`) - Implements `http.RoundTripper` - Automatically adds DPoP header to all requests - Handles nonce management and retries - Used by OAuth client for all HTTP requests **Token Store** (`pkg/auth/oauth/storage.go`) - Persists OAuth tokens and DPoP key - PEM encoding for private key - Expiration checking - Secure file permissions **Token Validator** (`pkg/auth/atproto/validator.go`) - `ValidateToken()` - Validate token via PDS getSession - `ValidateTokenWithResolver()` - Auto-resolve PDS from handle - Returns validated DID and handle - Used by registry to verify OAuth tokens ## Testing ### Manual Testing 1. Configure the helper: ```bash ./docker-credential-atcr configure # Enter handle: alice.bsky.social # Browser opens for authorization # Token saved to ~/.atcr/oauth-token.json ``` 2. Test credential retrieval: ```bash echo '{"ServerURL": "atcr.io"}' | ./docker-credential-atcr get # Should return: {"Username":"oauth2","Secret":""} ``` 3. Test with Docker: ```bash docker push atcr.io/alice/test:latest ``` ### Integration Testing TODO: Add automated tests for: - OAuth flow with mock PDS - DPoP proof generation - Token exchange - Credential helper protocol ## Security Features ### OAuth Token Validation The registry validates ATProto OAuth tokens by calling `com.atproto.server.getSession` on the user's PDS. This ensures: - Token is valid and not expired - Token belongs to the claimed user - User's DID and handle are extracted from the PDS response - No trust in client-provided identity information **Flow:** 1. Client sends OAuth token + handle to `/auth/exchange` 2. Registry resolves handle → PDS endpoint 3. Registry calls `{pds}/xrpc/com.atproto.server.getSession` with token 4. PDS validates token and returns session info (DID, handle) 5. Registry uses validated DID/handle to issue registry JWT ## Future Improvements 1. **Token refresh in background** - Proactively refresh before expiry - Reduce latency on Docker commands 3. **Multiple account support** - Store tokens for multiple handles - Allow selecting which account to use 4. **Revocation support** - Implement token revocation - Clean up on logout 5. **Better error messages** - User-friendly OAuth error handling - Guide users through common issues ## Troubleshooting ### "Failed to resolve identity" - Check internet connection - Verify handle is correct (e.g., `alice.bsky.social`) - Ensure PDS is accessible ### "Authorization timed out" - Complete authorization within 5 minutes - Check if browser opened correctly - Try running `configure` again ### "Token expired" - Credential helper should auto-refresh - If persistent, run `configure` again - Check `~/.atcr/oauth-token.json` permissions ### "Failed to exchange token" - Ensure registry is running - Check `/auth/exchange` endpoint is accessible - Verify token hasn't been revoked ## References - [ATProto OAuth Specification](https://atproto.com/specs/oauth) - [RFC 9449: DPoP](https://datatracker.ietf.org/doc/html/rfc9449) - [RFC 9126: PAR](https://datatracker.ietf.org/doc/html/rfc9126) - [RFC 7636: PKCE](https://datatracker.ietf.org/doc/html/rfc7636) - [Docker Credential Helpers](https://github.com/docker/docker-credential-helpers)