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

282 lines
8.3 KiB
Markdown

# 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": "<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:
```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":"<jwt>"}
```
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)