8.3 KiB
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
-
OAuth Client (
pkg/auth/oauth/)- Full ATProto OAuth implementation with DPoP support
- Uses
authelia.com/client/oauth2for OAuth + PAR - Uses
github.com/AxisCommunications/go-dpopfor DPoP proof generation - Automatic authorization server discovery
- PKCE support for security
-
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
- Standalone binary:
-
Registry Integration
/auth/exchangeendpoint exchanges OAuth tokens for registry JWTs- Existing
/auth/tokenendpoint 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
- Build the credential helper:
go build -o docker-credential-atcr ./cmd/credential-helper
- Install it in your PATH:
sudo mv docker-credential-atcr /usr/local/bin/
- Configure Docker to use it by editing
~/.docker/config.json:
{
"credsStore": "atcr"
}
Configuration
Run the OAuth flow:
docker-credential-atcr configure
This will:
- Prompt for your ATProto handle (e.g.,
alice.bsky.social) - Open your browser for OAuth authorization
- Store the OAuth token and DPoP key in
~/.atcr/oauth-token.json
Using with Docker
Once configured, use Docker normally:
# Push an image
docker push atcr.io/alice/myapp:latest
# Pull an image
docker pull atcr.io/alice/myapp:latest
The credential helper automatically:
- Loads your stored OAuth token
- Refreshes it if expired
- Exchanges it for a registry JWT
- Provides the JWT to Docker
How It Works
OAuth Flow
- User runs
docker-credential-atcr configure - Resolve identity: alice.bsky.social → DID → PDS endpoint
- Discover auth server: GET
{pds}/.well-known/oauth-authorization-server - Generate DPoP key: ECDSA P-256 key pair
- PAR request: POST to PAR endpoint with DPoP header + PKCE challenge
- Open browser: User authorizes on their PDS
- Receive code: Callback to
localhost:8888/callback - Exchange code: POST to token endpoint with DPoP header + PKCE verifier
- Save tokens: Store OAuth token + DPoP key + DID/handle
Docker Push/Pull Flow
- Docker needs credentials for
atcr.io - Calls credential helper:
docker-credential-atcr get - Helper loads token from
~/.atcr/oauth-token.json - Refresh if needed: Uses refresh token + DPoP if expired
- Exchange for registry JWT: POST to
/auth/exchangewith OAuth token + handle - Registry validates token: Calls
getSessionon PDS to validate token - Registry issues JWT: Creates registry JWT with validated DID/handle
- Return to Docker:
{"Username": "oauth2", "Secret": "<jwt>"} - 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 keyInitializeForHandle()- Discover auth serverAuthorizeURL()- Generate authorization URL with PAR + PKCEExchange()- Exchange code for token with DPoPRefreshToken()- 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 getSessionValidateTokenWithResolver()- Auto-resolve PDS from handle- Returns validated DID and handle
- Used by registry to verify OAuth tokens
Testing
Manual Testing
- Configure the helper:
./docker-credential-atcr configure
# Enter handle: alice.bsky.social
# Browser opens for authorization
# Token saved to ~/.atcr/oauth-token.json
- Test credential retrieval:
echo '{"ServerURL": "atcr.io"}' | ./docker-credential-atcr get
# Should return: {"Username":"oauth2","Secret":"<jwt>"}
- Test with Docker:
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:
- Client sends OAuth token + handle to
/auth/exchange - Registry resolves handle → PDS endpoint
- Registry calls
{pds}/xrpc/com.atproto.server.getSessionwith token - PDS validates token and returns session info (DID, handle)
- Registry uses validated DID/handle to issue registry JWT
Future Improvements
-
Token refresh in background
- Proactively refresh before expiry
- Reduce latency on Docker commands
-
Multiple account support
- Store tokens for multiple handles
- Allow selecting which account to use
-
Revocation support
- Implement token revocation
- Clean up on logout
-
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
configureagain
"Token expired"
- Credential helper should auto-refresh
- If persistent, run
configureagain - Check
~/.atcr/oauth-token.jsonpermissions
"Failed to exchange token"
- Ensure registry is running
- Check
/auth/exchangeendpoint is accessible - Verify token hasn't been revoked