mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-30 12:46:57 +00:00
282 lines
8.3 KiB
Markdown
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)
|