A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package oauth
2
3import (
4 "context"
5 "fmt"
6 "net/http"
7 "time"
8
9 "github.com/bluesky-social/indigo/atproto/auth/oauth"
10)
11
12// InteractiveResult contains the result of an interactive OAuth flow
13type InteractiveResult struct {
14 SessionData *oauth.ClientSessionData
15 Session *oauth.ClientSession
16 ClientApp *oauth.ClientApp
17}
18
19// InteractiveFlowWithCallback runs an interactive OAuth flow with explicit callback handling
20// This version allows the caller to register the callback handler before starting the flow
21func InteractiveFlowWithCallback(
22 ctx context.Context,
23 baseURL string,
24 handle string,
25 scopes []string,
26 registerCallback func(handler http.HandlerFunc) error,
27 displayAuthURL func(string) error,
28) (*InteractiveResult, error) {
29 store := oauth.NewMemStore()
30
31 // Create OAuth client app with custom scopes (or defaults if nil)
32 // Interactive flows are typically for production use (credential helper, etc.)
33 // For CLI tools, we use an empty keyPath since they're typically localhost (public client)
34 // or ephemeral sessions
35 if scopes == nil {
36 scopes = GetDefaultScopes("*")
37 }
38 clientApp, err := NewClientApp(baseURL, store, scopes, "", "AT Container Registry")
39 if err != nil {
40 return nil, fmt.Errorf("failed to create OAuth client app: %w", err)
41 }
42
43 // Channel to receive callback result
44 resultChan := make(chan *InteractiveResult, 1)
45 errorChan := make(chan error, 1)
46
47 // Create callback handler
48 callbackHandler := func(w http.ResponseWriter, r *http.Request) {
49 // Process callback
50 sessionData, err := clientApp.ProcessCallback(r.Context(), r.URL.Query())
51 if err != nil {
52 errorChan <- fmt.Errorf("failed to process callback: %w", err)
53 http.Error(w, "OAuth callback failed", http.StatusInternalServerError)
54 return
55 }
56
57 // Resume session
58 session, err := clientApp.ResumeSession(r.Context(), sessionData.AccountDID, sessionData.SessionID)
59 if err != nil {
60 errorChan <- fmt.Errorf("failed to resume session: %w", err)
61 http.Error(w, "Failed to resume session", http.StatusInternalServerError)
62 return
63 }
64
65 // Send result
66 resultChan <- &InteractiveResult{
67 SessionData: sessionData,
68 Session: session,
69 ClientApp: clientApp,
70 }
71
72 // Return success to browser
73 w.Header().Set("Content-Type", "text/html")
74 fmt.Fprintf(w, "<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>")
75 }
76
77 // Register callback handler
78 if err := registerCallback(callbackHandler); err != nil {
79 return nil, fmt.Errorf("failed to register callback: %w", err)
80 }
81
82 // Start auth flow
83 authURL, err := clientApp.StartAuthFlow(ctx, handle)
84 if err != nil {
85 return nil, fmt.Errorf("failed to start auth flow: %w", err)
86 }
87
88 // Display auth URL
89 if err := displayAuthURL(authURL); err != nil {
90 return nil, fmt.Errorf("failed to display auth URL: %w", err)
91 }
92
93 // Wait for callback result
94 select {
95 case result := <-resultChan:
96 return result, nil
97 case err := <-errorChan:
98 return nil, err
99 case <-time.After(5 * time.Minute):
100 return nil, fmt.Errorf("OAuth flow timed out after 5 minutes")
101 }
102}