Files
at-container-registry/pkg/auth/oauth/flow.go
T

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
}