Files
at-container-registry/docs/OAUTH.md
T
2025-10-02 11:03:59 -05:00

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

  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:
go build -o docker-credential-atcr ./cmd/credential-helper
  1. Install it in your PATH:
sudo mv docker-credential-atcr /usr/local/bin/
  1. Configure Docker to use it by editing ~/.docker/config.json:
{
  "credsStore": "atcr"
}

Configuration

Run the OAuth flow:

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:

# 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": "<jwt>"}
  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:
./docker-credential-atcr configure
# Enter handle: alice.bsky.social
# Browser opens for authorization
# Token saved to ~/.atcr/oauth-token.json
  1. Test credential retrieval:
echo '{"ServerURL": "atcr.io"}' | ./docker-credential-atcr get
# Should return: {"Username":"oauth2","Secret":"<jwt>"}
  1. 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:

  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
  2. Multiple account support

    • Store tokens for multiple handles
    • Allow selecting which account to use
  3. Revocation support

    • Implement token revocation
    • Clean up on logout
  4. 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