A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
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.