110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package oauth
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/bluesky-social/indigo/atproto/auth/oauth"
|
|
)
|
|
|
|
// InteractiveResult contains the result of an interactive OAuth flow
|
|
type InteractiveResult struct {
|
|
SessionData *oauth.ClientSessionData
|
|
Session *oauth.ClientSession
|
|
App *App
|
|
}
|
|
|
|
// InteractiveFlowWithCallback runs an interactive OAuth flow with explicit callback handling
|
|
// This version allows the caller to register the callback handler before starting the flow
|
|
func InteractiveFlowWithCallback(
|
|
ctx context.Context,
|
|
baseURL string,
|
|
handle string,
|
|
scopes []string,
|
|
registerCallback func(handler http.HandlerFunc) error,
|
|
displayAuthURL func(string) error,
|
|
) (*InteractiveResult, error) {
|
|
// Create temporary file store for this flow
|
|
store, err := NewFileStore("/tmp/atcr-oauth-temp.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OAuth store: %w", err)
|
|
}
|
|
|
|
// Create OAuth app with custom scopes (or defaults if nil)
|
|
// Interactive flows are typically for production use (credential helper, etc.)
|
|
// so we default to testMode=false
|
|
// For CLI tools, we use an empty keyPath since they're typically localhost (public client)
|
|
// or ephemeral sessions
|
|
var app *App
|
|
if scopes != nil {
|
|
app, err = NewAppWithScopes(baseURL, store, scopes, "", "AT Container Registry")
|
|
} else {
|
|
app, err = NewApp(baseURL, store, "*", "", "AT Container Registry")
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create OAuth app: %w", err)
|
|
}
|
|
|
|
// Channel to receive callback result
|
|
resultChan := make(chan *InteractiveResult, 1)
|
|
errorChan := make(chan error, 1)
|
|
|
|
// Create callback handler
|
|
callbackHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
// Process callback
|
|
sessionData, err := app.ProcessCallback(r.Context(), r.URL.Query())
|
|
if err != nil {
|
|
errorChan <- fmt.Errorf("failed to process callback: %w", err)
|
|
http.Error(w, "OAuth callback failed", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Resume session
|
|
session, err := app.ResumeSession(r.Context(), sessionData.AccountDID, sessionData.SessionID)
|
|
if err != nil {
|
|
errorChan <- fmt.Errorf("failed to resume session: %w", err)
|
|
http.Error(w, "Failed to resume session", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
// Send result
|
|
resultChan <- &InteractiveResult{
|
|
SessionData: sessionData,
|
|
Session: session,
|
|
App: app,
|
|
}
|
|
|
|
// Return success to browser
|
|
w.Header().Set("Content-Type", "text/html")
|
|
fmt.Fprintf(w, "<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>")
|
|
}
|
|
|
|
// Register callback handler
|
|
if err := registerCallback(callbackHandler); err != nil {
|
|
return nil, fmt.Errorf("failed to register callback: %w", err)
|
|
}
|
|
|
|
// Start auth flow
|
|
authURL, err := app.StartAuthFlow(ctx, handle)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to start auth flow: %w", err)
|
|
}
|
|
|
|
// Display auth URL
|
|
if err := displayAuthURL(authURL); err != nil {
|
|
return nil, fmt.Errorf("failed to display auth URL: %w", err)
|
|
}
|
|
|
|
// Wait for callback result
|
|
select {
|
|
case result := <-resultChan:
|
|
return result, nil
|
|
case err := <-errorChan:
|
|
return nil, err
|
|
case <-time.After(5 * time.Minute):
|
|
return nil, fmt.Errorf("OAuth flow timed out after 5 minutes")
|
|
}
|
|
}
|