A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
80
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor oauth code to reduce complexity

+147 -222
+41 -11
CLAUDE.md
··· 167 167 - **PAR** (RFC 9126) - Pushed Authorization Requests for server-to-server parameter exchange 168 168 - **PKCE** (RFC 7636) - Proof Key for Code Exchange to prevent authorization code interception 169 169 170 - **Key Components** (`pkg/auth/`): 170 + **Key Components** (`pkg/auth/oauth/`): 171 + 172 + 1. **Client** (`client.go`) - Core OAuth client with encapsulated configuration 173 + - Constructor: `NewClient(baseURL)` - accepts base URL, derives client ID/redirect URI 174 + - `NewClientWithKey(baseURL, dpopKey)` - for token refresh with stored DPoP key 175 + - `ClientID()` - computes localhost vs production client ID dynamically 176 + - `RedirectURI()` - returns `baseURL + "/auth/oauth/callback"` 177 + - `GetDefaultScopes()` - returns ATCR registry scopes 178 + - All OAuth flows (authorization, token exchange, refresh) in one place 171 179 172 - 1. **OAuth Client** (`oauth/client.go`) - Handles authorization flow with DPoP 173 - 2. **DPoP Transport** (`oauth/transport.go`) - HTTP RoundTripper that auto-adds DPoP headers 174 - 3. **Token Storage** (`oauth/storage.go`) - Persists tokens and DPoP key in `~/.atcr/oauth-token.json` 175 - 4. **Token Validator** (`atproto/validator.go`) - Validates tokens via PDS `getSession` endpoint 176 - 5. **Exchange Handler** (`exchange/handler.go`) - Exchanges OAuth tokens for registry JWTs 180 + 2. **DPoP Transport** (`transport.go`) - HTTP RoundTripper that auto-adds DPoP headers 181 + 182 + 3. **Token Storage** (`tokenstorage.go`) - Persists refresh tokens and DPoP keys for AppView 183 + - File-based storage in `/var/lib/atcr/refresh-tokens.json` (AppView) 184 + - Client uses `~/.atcr/oauth-token.json` (credential helper) 185 + 186 + 4. **Refresher** (`refresher.go`) - Token refresh manager for AppView 187 + - Caches access tokens with automatic refresh 188 + - Per-DID locking prevents concurrent refresh races 189 + - Uses Client methods for consistency 190 + 191 + 5. **Server** (`server.go`) - OAuth authorization endpoints for AppView 192 + - `GET /auth/oauth/authorize` - starts OAuth flow 193 + - `GET /auth/oauth/callback` - handles OAuth callback 194 + - Uses Client methods for authorization and token exchange 195 + 196 + 6. **Interactive Flow** (`flow.go`) - Reusable OAuth flow for CLI tools 197 + - Used by credential helper and hold service registration 198 + - Two-phase callback setup ensures PAR metadata availability 177 199 178 200 **Authentication Flow:** 179 201 ``` ··· 398 420 399 421 ### Development Notes 400 422 423 + **General:** 401 424 - Middleware is registered via `init()` functions in `pkg/middleware/` 402 425 - Import `_ "atcr.io/pkg/middleware"` in main.go to register middleware 403 426 - Storage drivers imported as `_ "github.com/distribution/distribution/v3/registry/storage/driver/s3-aws"` 404 427 - Storage service reuses distribution's driver factory for multi-backend support 405 - - OAuth client uses `authelia.com/client/oauth2` for PAR support 428 + 429 + **OAuth implementation:** 430 + - Client (`pkg/auth/oauth/client.go`) encapsulates all OAuth configuration 431 + - Uses `authelia.com/client/oauth2` for PAR support 406 432 - DPoP proofs generated with `github.com/AxisCommunications/go-dpop` (auto-handles JWK) 407 433 - Token validation via `com.atproto.server.getSession` ensures no trust in client-provided identity 434 + - All ATCR components use standardized `/auth/oauth/callback` path 435 + - Client ID generation (localhost query-based vs production metadata URL) handled internally 408 436 409 437 ### Testing Strategy 410 438 ··· 432 460 2. Update `pkg/middleware/registry.go` if changing routing logic 433 461 3. Remember: `findStorageEndpoint()` queries PDS for `io.atcr.hold` records 434 462 435 - **Implementing OAuth authentication**: 436 - - AppView: `pkg/auth/exchange/handler.go` - validates tokens via PDS getSession 437 - - Client: `pkg/auth/oauth/client.go` - OAuth + DPoP flow 438 - - Helper: `cmd/credential-helper/` - Docker credential protocol 463 + **Working with OAuth client**: 464 + - Client is self-contained: pass `baseURL`, it handles client ID/redirect URI/scopes 465 + - For AppView server/refresher: use `NewClient(baseURL)` or `NewClientWithKey(baseURL, storedKey)` 466 + - For custom scopes: call `client.SetScopes(customScopes)` after initialization 467 + - Standard callback path: `/auth/oauth/callback` (used by all ATCR components) 468 + - Client methods are consistent across authorization, token exchange, and refresh flows 439 469 440 470 **Adding BYOS support for a user**: 441 471 1. User sets environment variables (storage credentials, public URL)
+7 -16
cmd/hold/main.go
··· 862 862 fmt.Sprintf("repo:%s?action=update", atproto.HoldCrewCollection), 863 863 } 864 864 865 - // Determine base URL and client ID based on mode 866 - var baseURL, callbackPath string 867 - callbackPath = "/oauth/callback" 865 + // Determine base URL based on mode 866 + // Callback path standardized to /auth/oauth/callback across ATCR 867 + var baseURL string 868 868 869 869 if s.config.Server.TestMode { 870 870 // Test mode: Use localhost for OAuth (browser accessible) but store real URL in hold record ··· 882 882 baseURL = publicURL 883 883 } 884 884 885 - // Use shared helper to construct client ID 886 - cfg := oauth.ClientIDConfig{ 887 - BaseURL: baseURL, 888 - CallbackPath: callbackPath, 889 - Scopes: holdScopes, 890 - } 891 - clientID, redirectURI := cfg.MakeClientID() 892 - 893 885 // Run interactive OAuth flow with persistent server 894 886 ctx := context.Background() 895 887 result, err := oauth.RunInteractiveFlow( 896 888 ctx, 897 889 oauth.InteractiveFlowConfig{ 898 - ClientID: clientID, 899 - RedirectURI: redirectURI, 900 - Handle: handle, 901 - Scopes: holdScopes, 890 + BaseURL: baseURL, 891 + Handle: handle, 892 + Scopes: holdScopes, 902 893 }, 903 894 func(authURL string, handler *oauth.CallbackHandler, metadata *oauth.ClientMetadata) error { 904 895 // First call (authURL empty): register callback handler 905 896 if authURL == "" { 906 897 // Register callback on existing server (persistent server pattern) 907 898 // Note: metadata is not used here since hold service serves it separately on main server 908 - http.HandleFunc("/oauth/callback", handler.ServeHTTP) 899 + http.HandleFunc("/auth/oauth/callback", handler.ServeHTTP) 909 900 return nil 910 901 } 911 902
+5 -20
cmd/registry/serve.go
··· 108 108 109 109 fmt.Printf("DEBUG: Base URL for OAuth: %s\n", baseURL) 110 110 111 - // 4. Get client ID from config 112 - clientIDConfig := oauth.ClientIDConfig{ 113 - BaseURL: baseURL, 114 - CallbackPath: "/auth/oauth/callback", 115 - Scopes: oauth.GetDefaultScopes(), 116 - } 117 - clientID, redirectURI := clientIDConfig.MakeClientID() 118 - 119 - fmt.Printf("DEBUG: Client ID: %s\n", clientID) 120 - fmt.Printf("DEBUG: Redirect URI: %s\n", redirectURI) 121 - 122 - // 5. Create refresher 123 - refresher := oauth.NewRefresher(refreshStorage, clientID, redirectURI) 111 + // 4. Create refresher 112 + refresher := oauth.NewRefresher(refreshStorage, baseURL) 124 113 // Start cleanup routine (runs every hour) 125 114 refresher.StartCleanupRoutine(1 * time.Hour) 126 115 127 - // 6. Set global refresher for middleware 116 + // 5. Set global refresher for middleware 128 117 middleware.SetGlobalRefresher(refresher) 129 118 130 - // 7. Create client metadata (only needed for production, not localhost) 131 - // For localhost, client metadata is embedded in the client_id query string 132 - // clientMetadata := oauth.NewClientMetadata(clientID, []string{redirectURI}) 133 - 134 - // 8. Create OAuth server 119 + // 6. Create OAuth server 135 120 oauthServer := oauth.NewServer(refreshStorage, sessionManager, baseURL) 136 121 // Connect server to refresher for cache invalidation 137 122 oauthServer.SetRefresher(refresher) 138 123 139 - // 9. Initialize auth keys and create token issuer 124 + // 7. Initialize auth keys and create token issuer 140 125 var issuer *token.Issuer 141 126 if config.Auth["token"] != nil { 142 127 if err := initializeAuthKeys(config); err != nil {
hold

This is a binary file and will not be displayed.

+67 -19
pkg/auth/oauth/client.go
··· 10 10 "fmt" 11 11 "net/http" 12 12 13 + "net/url" 14 + "strings" 15 + 13 16 "atcr.io/pkg/atproto" 14 17 "authelia.com/client/oauth2" 15 18 ) ··· 20 23 dpopKey *ecdsa.PrivateKey 21 24 dpopTransport *DPoPTransport 22 25 resolver *atproto.Resolver 23 - clientID string 24 - redirectURI string 26 + baseUrl string 25 27 metadata *AuthServerMetadata 26 28 } 27 29 28 - // NewClient creates a new OAuth client for ATProto 29 - func NewClient(clientID, redirectURI string) (*Client, error) { 30 + // NewClient creates a new OAuth client for ATProto from a base URL 31 + func NewClient(baseURL string) (*Client, error) { 30 32 // Generate DPoP key 31 33 dpopKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 32 34 if err != nil { 33 35 return nil, fmt.Errorf("failed to generate DPoP key: %w", err) 34 36 } 35 37 38 + return NewClientWithKey(baseURL, dpopKey), nil 39 + } 40 + 41 + // NewClientWithKey creates a new OAuth client with an existing DPoP key 42 + // This is useful when working with stored credentials (e.g., in AppView token refresh) 43 + func NewClientWithKey(baseURL string, dpopKey *ecdsa.PrivateKey) *Client { 36 44 return &Client{ 37 45 dpopKey: dpopKey, 38 46 dpopTransport: NewDPoPTransport(http.DefaultTransport, dpopKey), 39 47 resolver: atproto.NewResolver(), 40 - clientID: clientID, 41 - redirectURI: redirectURI, 42 - }, nil 48 + baseUrl: baseURL, 49 + } 43 50 } 44 51 45 52 // InitializeForHandle discovers the authorization server for a given handle/DID ··· 50 57 return fmt.Errorf("failed to resolve identity: %w", err) 51 58 } 52 59 60 + return c.InitializeForPDS(ctx, pdsEndpoint) 61 + } 62 + 63 + // InitializeForPDS discovers the authorization server for a given PDS endpoint 64 + // This is useful when you already know the PDS endpoint (e.g., from stored credentials) 65 + func (c *Client) InitializeForPDS(ctx context.Context, pdsEndpoint string) error { 53 66 // Discover authorization server metadata 54 67 metadata, err := DiscoverAuthServer(ctx, pdsEndpoint) 55 68 if err != nil { ··· 59 72 c.metadata = metadata 60 73 61 74 // Configure OAuth2 client 62 - // Note: Both localhost and production need redirect_uri and scopes in the config 63 - // For localhost: client_id contains these (query-based) AND they're sent as params 64 - // For production: client_id is metadata URL, params come from config 65 75 c.config = &oauth2.Config{ 66 - ClientID: c.clientID, 76 + ClientID: c.ClientID(), 67 77 Endpoint: oauth2.Endpoint{ 68 78 AuthURL: metadata.AuthorizationEndpoint, 69 79 TokenURL: metadata.TokenEndpoint, 70 80 PushedAuthURL: metadata.PushedAuthorizationRequestEndpoint, 71 81 }, 72 - RedirectURL: c.redirectURI, 73 - Scopes: GetDefaultScopes(), 82 + RedirectURL: c.RedirectURI(), 83 + Scopes: c.GetDefaultScopes(), 74 84 } 75 85 76 86 return nil ··· 177 187 Transport: c.dpopTransport, 178 188 }) 179 189 180 - // Create a token source with the refresh token 181 - token := &oauth2.Token{ 182 - RefreshToken: refreshToken, 183 - } 184 - 185 190 // Refresh the token 186 - newToken, err := c.config.TokenSource(ctx, token).Token() 191 + newToken, err := c.config.TokenSource(ctx, &oauth2.Token{ 192 + RefreshToken: refreshToken, 193 + }).Token() 187 194 if err != nil { 188 195 return nil, fmt.Errorf("failed to refresh token: %w", err) 189 196 } 190 197 198 + // Set access token on transport for "ath" claim in future DPoP proofs 199 + c.dpopTransport.SetAccessToken(newToken.AccessToken) 200 + 191 201 return newToken, nil 192 202 } 193 203 204 + func (c *Client) ClientID() (string) { 205 + return c.ClientIDWithScopes(c.GetDefaultScopes()) 206 + } 207 + 208 + func (c *Client) ClientIDWithScopes(scopes []string) string { 209 + scopeStr := strings.Join(scopes, " ") 210 + if strings.Contains(c.baseUrl, "127.0.0.1") || strings.Contains(c.baseUrl, "localhost") { 211 + // Localhost: use query-based client ID 212 + return fmt.Sprintf("http://localhost?redirect_uri=%s&scope=%s", 213 + url.QueryEscape(c.RedirectURI()), 214 + url.QueryEscape(scopeStr)) 215 + } 216 + // Production: use metadata URL 217 + return c.baseUrl + "/client-metadata.json" 218 + } 219 + 220 + func (c *Client) RedirectURI() string { 221 + return c.baseUrl + "/auth/oauth/callback" 222 + } 223 + 194 224 // DPoPKey returns the DPoP private key 195 225 func (c *Client) DPoPKey() *ecdsa.PrivateKey { 196 226 return c.dpopKey 197 227 } 198 228 229 + // DPoPTransport returns the DPoP transport 230 + func (c *Client) DPoPTransport() *DPoPTransport { 231 + return c.dpopTransport 232 + } 233 + 199 234 // SetDPoPKey sets the DPoP private key (useful when loading from storage) 200 235 func (c *Client) SetDPoPKey(key *ecdsa.PrivateKey) { 201 236 c.dpopKey = key 202 237 c.dpopTransport = NewDPoPTransport(http.DefaultTransport, key) 238 + } 239 + 240 + // GetDefaultScopes returns the default OAuth scopes for ATCR registry operations 241 + func (c *Client) GetDefaultScopes() []string { 242 + return []string{ 243 + "atproto", 244 + "transition:generic.full", 245 + "blob:application/vnd.docker.distribution.manifest.v2+json", 246 + fmt.Sprintf("repo:%s?action=create", atproto.ManifestCollection), 247 + fmt.Sprintf("repo:%s?action=update", atproto.ManifestCollection), 248 + fmt.Sprintf("repo:%s?action=create", atproto.TagCollection), 249 + fmt.Sprintf("repo:%s?action=update", atproto.TagCollection), 250 + } 203 251 } 204 252 205 253 // generateCodeVerifier generates a PKCE code verifier
-52
pkg/auth/oauth/client_id.go
··· 1 - package oauth 2 - 3 - import ( 4 - "fmt" 5 - "net/url" 6 - "strings" 7 - ) 8 - 9 - // MakeLocalhostClientID creates a query-based client ID for localhost development 10 - // Per ATProto OAuth spec: http://localhost?redirect_uri=...&scope=... 11 - func MakeLocalhostClientID(redirectURI string, scopes string) string { 12 - return fmt.Sprintf("http://localhost?redirect_uri=%s&scope=%s", 13 - url.QueryEscape(redirectURI), 14 - url.QueryEscape(scopes)) 15 - } 16 - 17 - // MakeProductionClientID creates a metadata URL client ID for production 18 - // Format: https://example.com/client-metadata.json 19 - func MakeProductionClientID(baseURL string) string { 20 - return baseURL + "/client-metadata.json" 21 - } 22 - 23 - // IsLocalhostURL checks if a URL is localhost/127.0.0.1 24 - func IsLocalhostURL(urlStr string) bool { 25 - return strings.Contains(urlStr, "127.0.0.1") || strings.Contains(urlStr, "localhost") 26 - } 27 - 28 - // ClientIDConfig helps construct appropriate client IDs for different environments 29 - type ClientIDConfig struct { 30 - BaseURL string // Base URL (e.g., "http://127.0.0.1:8888" or "https://example.com") 31 - CallbackPath string // Callback path (e.g., "/oauth/callback") 32 - Scopes []string // OAuth scopes 33 - } 34 - 35 - // MakeClientID creates the appropriate client ID based on the environment 36 - // Returns (clientID, redirectURI) 37 - func (c *ClientIDConfig) MakeClientID() (string, string) { 38 - redirectURI := c.BaseURL + c.CallbackPath 39 - scopeStr := strings.Join(c.Scopes, " ") 40 - 41 - if scopeStr == "" { 42 - scopeStr = "atproto" 43 - } 44 - 45 - if IsLocalhostURL(c.BaseURL) { 46 - // Localhost: use query-based client ID 47 - return MakeLocalhostClientID(redirectURI, scopeStr), redirectURI 48 - } 49 - 50 - // Production: use metadata URL 51 - return MakeProductionClientID(c.BaseURL), redirectURI 52 - }
+6 -7
pkg/auth/oauth/flow.go
··· 10 10 11 11 // InteractiveFlowConfig configures an interactive OAuth flow 12 12 type InteractiveFlowConfig struct { 13 - ClientID string 14 - RedirectURI string 15 - Handle string 16 - Scopes []string // optional, defaults to ["atproto"] 13 + BaseURL string // Base URL for OAuth callbacks (e.g., "http://127.0.0.1:8080") 14 + Handle string // ATProto handle or DID 15 + Scopes []string // Optional, defaults to GetDefaultScopes() 17 16 } 18 17 19 18 // FlowResult contains the result of a successful OAuth flow ··· 31 30 func RunInteractiveFlow(ctx context.Context, cfg InteractiveFlowConfig, 32 31 setupCallback func(authURL string, handler *CallbackHandler, metadata *ClientMetadata) error) (*FlowResult, error) { 33 32 34 - // Create OAuth client 35 - client, err := NewClient(cfg.ClientID, cfg.RedirectURI) 33 + // Create OAuth client from base URL 34 + client, err := NewClient(cfg.BaseURL) 36 35 if err != nil { 37 36 return nil, fmt.Errorf("failed to create OAuth client: %w", err) 38 37 } ··· 58 57 59 58 // Create callback handler and client metadata FIRST 60 59 callbackHandler := NewCallbackHandler(state) 61 - metadata := NewClientMetadata(cfg.ClientID, []string{cfg.RedirectURI}) 60 + metadata := NewClientMetadata(client.ClientID(), []string{client.RedirectURI()}) 62 61 63 62 // Start server BEFORE generating auth URL (so PAR can fetch metadata) 64 63 if err := setupCallback("", callbackHandler, metadata); err != nil {
+12 -36
pkg/auth/oauth/refresher.go
··· 4 4 "context" 5 5 "crypto/ecdsa" 6 6 "fmt" 7 - "net/http" 8 7 "sync" 9 8 "time" 10 - 11 - "authelia.com/client/oauth2" 12 9 ) 13 10 14 11 // AccessTokenEntry represents a cached access token ··· 26 23 mu sync.RWMutex 27 24 refreshLocks map[string]*sync.Mutex // Per-DID locks for refresh operations 28 25 refreshLockMu sync.Mutex // Protects refreshLocks map 29 - clientID string 30 - redirectURI string 26 + baseURL string 31 27 } 32 28 33 29 // NewRefresher creates a new token refresher 34 - func NewRefresher(storage *RefreshTokenStorage, clientID, redirectURI string) *Refresher { 30 + func NewRefresher(storage *RefreshTokenStorage, baseURL string) *Refresher { 35 31 return &Refresher{ 36 32 storage: storage, 37 33 accessTokens: make(map[string]*AccessTokenEntry), 38 34 refreshLocks: make(map[string]*sync.Mutex), 39 - clientID: clientID, 40 - redirectURI: redirectURI, 35 + baseURL: baseURL, 41 36 } 42 37 } 43 38 ··· 98 93 return "", nil, nil, fmt.Errorf("failed to get DPoP key: %w", err) 99 94 } 100 95 101 - // Create OAuth client with DPoP transport 102 - dpopTransport := NewDPoPTransport(http.DefaultTransport, dpopKey) 103 - httpClient := &http.Client{Transport: dpopTransport} 96 + // Create OAuth client with stored DPoP key 97 + client := NewClientWithKey(r.baseURL, dpopKey) 104 98 105 - // Discover PDS OAuth metadata 106 - metadata, err := DiscoverAuthServer(ctx, entry.PDS) 107 - if err != nil { 108 - return "", nil, nil, fmt.Errorf("failed to discover auth server: %w", err) 109 - } 110 - 111 - // Configure OAuth2 client 112 - config := &oauth2.Config{ 113 - ClientID: r.clientID, 114 - Endpoint: oauth2.Endpoint{ 115 - AuthURL: metadata.AuthorizationEndpoint, 116 - TokenURL: metadata.TokenEndpoint, 117 - PushedAuthURL: metadata.PushedAuthorizationRequestEndpoint, 118 - }, 119 - RedirectURL: r.redirectURI, 120 - Scopes: GetDefaultScopes(), 99 + // Initialize for PDS endpoint 100 + if err := client.InitializeForPDS(ctx, entry.PDS); err != nil { 101 + return "", nil, nil, fmt.Errorf("failed to initialize OAuth client: %w", err) 121 102 } 122 103 123 - // Create context with custom HTTP client 124 - ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, httpClient) 125 - 126 - // Exchange refresh token for new access token 127 - token, err := config.TokenSource(ctxWithClient, &oauth2.Token{ 128 - RefreshToken: entry.RefreshToken, 129 - }).Token() 104 + // Refresh the token 105 + token, err := client.RefreshToken(ctx, entry.RefreshToken) 130 106 if err != nil { 131 107 return "", nil, nil, fmt.Errorf("failed to refresh token: %w", err) 132 108 } ··· 146 122 } 147 123 } 148 124 149 - // Set access token on transport for "ath" claim in future DPoP proofs 150 - dpopTransport.SetAccessToken(token.AccessToken) 125 + // Get DPoP transport (already has access token set by client.RefreshToken) 126 + dpopTransport := client.DPoPTransport() 151 127 152 128 // Cache the access token and transport 153 129 // Expire 1 minute early to avoid edge cases
-22
pkg/auth/oauth/scopes.go
··· 1 - package oauth 2 - 3 - import ( 4 - "fmt" 5 - 6 - "atcr.io/pkg/atproto" 7 - ) 8 - 9 - // GetDefaultScopes returns the default OAuth scopes for ATCR registry operations 10 - func GetDefaultScopes() []string { 11 - return []string{ 12 - "atproto", 13 - "transition:generic.full", 14 - "blob:application/vnd.docker.distribution.manifest.v2+json", 15 - "blob:application/vnd.docker.image.rootfs.diff.tar.gzip", 16 - "blob:application/vnd.docker.container.image.v1+json", 17 - fmt.Sprintf("repo:%s?action=create", atproto.ManifestCollection), 18 - fmt.Sprintf("repo:%s?action=update", atproto.ManifestCollection), 19 - fmt.Sprintf("repo:%s?action=create", atproto.TagCollection), 20 - fmt.Sprintf("repo:%s?action=update", atproto.TagCollection), 21 - } 22 - }
+9 -39
pkg/auth/oauth/server.go
··· 12 12 13 13 "atcr.io/pkg/atproto" 14 14 "atcr.io/pkg/auth/session" 15 - "authelia.com/client/oauth2" 16 15 ) 17 16 18 17 // Server handles OAuth authorization for the AppView ··· 21 20 sessionManager *session.Manager 22 21 resolver *atproto.Resolver 23 22 refresher *Refresher 24 - clientID string 25 - redirectURI string 26 23 baseURL string 27 24 states map[string]*OAuthState 28 25 statesMu sync.RWMutex ··· 41 38 42 39 // NewServer creates a new OAuth server 43 40 func NewServer(storage *RefreshTokenStorage, sessionManager *session.Manager, baseURL string) *Server { 44 - // Create client ID based on AppView's base URL 45 - cfg := ClientIDConfig{ 46 - BaseURL: baseURL, 47 - CallbackPath: "/auth/oauth/callback", 48 - Scopes: GetDefaultScopes(), 49 - } 50 - clientID, redirectURI := cfg.MakeClientID() 51 - 52 41 return &Server{ 53 42 storage: storage, 54 43 sessionManager: sessionManager, 55 44 resolver: atproto.NewResolver(), 56 45 refresher: nil, // Will be set via SetRefresher() 57 - clientID: clientID, 58 - redirectURI: redirectURI, 59 46 baseURL: baseURL, 60 47 states: make(map[string]*OAuthState), 61 48 } ··· 92 79 93 80 fmt.Printf("DEBUG [oauth/server]: Resolved handle=%s -> did=%s, pds=%s\n", handle, did, pdsEndpoint) 94 81 95 - // Create OAuth client 96 - fmt.Printf("DEBUG [oauth/server]: Creating OAuth client with clientID=%s, redirectURI=%s\n", s.clientID, s.redirectURI) 97 - client, err := NewClient(s.clientID, s.redirectURI) 82 + // Create OAuth client from base URL 83 + fmt.Printf("DEBUG [oauth/server]: Creating OAuth client for baseURL=%s\n", s.baseURL) 84 + client, err := NewClient(s.baseURL) 98 85 if err != nil { 99 86 fmt.Printf("ERROR [oauth/server]: Failed to create OAuth client: %v\n", err) 100 87 http.Error(w, fmt.Sprintf("failed to create OAuth client: %v", err), http.StatusInternalServerError) ··· 178 165 179 166 // exchangeCodeForSession exchanges authorization code for tokens and creates session 180 167 func (s *Server) exchangeCodeForSession(ctx context.Context, code string, state *OAuthState) (string, error) { 181 - // Discover OAuth metadata 182 - metadata, err := DiscoverAuthServer(ctx, state.PDSEndpoint) 183 - if err != nil { 184 - return "", fmt.Errorf("failed to discover auth server: %w", err) 185 - } 186 - 187 - // Create DPoP transport 188 - dpopTransport := NewDPoPTransport(http.DefaultTransport, state.DPoPKey) 189 - httpClient := &http.Client{Transport: dpopTransport} 168 + // Create OAuth client with stored DPoP key 169 + client := NewClientWithKey(s.baseURL, state.DPoPKey) 190 170 191 - // Configure OAuth2 client 192 - config := &oauth2.Config{ 193 - ClientID: s.clientID, 194 - Endpoint: oauth2.Endpoint{ 195 - AuthURL: metadata.AuthorizationEndpoint, 196 - TokenURL: metadata.TokenEndpoint, 197 - PushedAuthURL: metadata.PushedAuthorizationRequestEndpoint, 198 - }, 199 - RedirectURL: s.redirectURI, 200 - Scopes: GetDefaultScopes(), 171 + // Initialize for PDS endpoint 172 + if err := client.InitializeForPDS(ctx, state.PDSEndpoint); err != nil { 173 + return "", fmt.Errorf("failed to initialize OAuth client: %w", err) 201 174 } 202 175 203 - // Create context with custom HTTP client 204 - ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, httpClient) 205 - 206 176 // Exchange code for token 207 - token, err := config.Exchange(ctxWithClient, code, oauth2.VerifierOption(state.CodeVerifier)) 177 + token, err := client.Exchange(ctx, code, state.CodeVerifier) 208 178 if err != nil { 209 179 return "", fmt.Errorf("failed to exchange code: %w", err) 210 180 }
registry

This is a binary file and will not be displayed.