mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-19 08:44:14 +00:00
98 lines
2.9 KiB
Go
98 lines
2.9 KiB
Go
package oauth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"authelia.com/client/oauth2"
|
|
)
|
|
|
|
// InteractiveFlowConfig configures an interactive OAuth flow
|
|
type InteractiveFlowConfig struct {
|
|
BaseURL string // Base URL for OAuth callbacks (e.g., "http://127.0.0.1:8080")
|
|
Handle string // ATProto handle or DID
|
|
Scopes []string // Optional, defaults to GetDefaultScopes()
|
|
}
|
|
|
|
// FlowResult contains the result of a successful OAuth flow
|
|
type FlowResult struct {
|
|
Token *oauth2.Token
|
|
Client *Client // OAuth client with DPoP key set
|
|
}
|
|
|
|
// RunInteractiveFlow executes an interactive OAuth authorization code flow
|
|
// The setupCallback function is called TWICE:
|
|
// 1. First with authURL="" to start the server (before PAR)
|
|
// 2. Then with the actual authURL to display it to the user (after PAR)
|
|
//
|
|
// This two-phase approach ensures the server is running before PAR tries to fetch client metadata
|
|
func RunInteractiveFlow(ctx context.Context, cfg InteractiveFlowConfig,
|
|
setupCallback func(authURL string, handler *CallbackHandler, metadata *ClientMetadata) error) (*FlowResult, error) {
|
|
|
|
// Create OAuth client from base URL
|
|
client, err := NewClient(cfg.BaseURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OAuth client: %w", err)
|
|
}
|
|
|
|
// Initialize for the given handle
|
|
initCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := client.InitializeForHandle(initCtx, cfg.Handle); err != nil {
|
|
return nil, fmt.Errorf("failed to initialize client: %w", err)
|
|
}
|
|
|
|
// Set scopes if provided
|
|
if len(cfg.Scopes) > 0 {
|
|
client.SetScopes(cfg.Scopes)
|
|
}
|
|
|
|
// Generate state for OAuth flow
|
|
state, err := GenerateState()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate state: %w", err)
|
|
}
|
|
|
|
// Create callback handler and client metadata FIRST
|
|
callbackHandler := NewCallbackHandler(state)
|
|
metadata := NewClientMetadata(client.ClientID(), []string{client.RedirectURI()})
|
|
|
|
// Start server BEFORE generating auth URL (so PAR can fetch metadata)
|
|
if err := setupCallback("", callbackHandler, metadata); err != nil {
|
|
return nil, fmt.Errorf("callback setup failed: %w", err)
|
|
}
|
|
|
|
// NOW generate authorization URL with PKCE (PAR can succeed)
|
|
authURL, codeVerifier, err := client.AuthorizeURL(state)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate auth URL: %w", err)
|
|
}
|
|
|
|
// Display the auth URL (callback gets called again with URL)
|
|
if err := setupCallback(authURL, callbackHandler, metadata); err != nil {
|
|
return nil, fmt.Errorf("failed to display auth URL: %w", err)
|
|
}
|
|
|
|
// Wait for callback (5 minute timeout)
|
|
code, err := callbackHandler.WaitForCode(5 * time.Minute)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Exchange code for token
|
|
exchangeCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancel()
|
|
|
|
token, err := client.Exchange(exchangeCtx, code, codeVerifier)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to exchange code: %w", err)
|
|
}
|
|
|
|
return &FlowResult{
|
|
Token: token,
|
|
Client: client,
|
|
}, nil
|
|
}
|