A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
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 App *App
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 // Create temporary file store for this flow
30 store, err := NewFileStore("/tmp/atcr-oauth-temp.json")
31 if err != nil {
32 return nil, fmt.Errorf("failed to create OAuth store: %w", err)
33 }
34
35 // Create OAuth app with custom scopes (or defaults if nil)
36 // Interactive flows are typically for production use (credential helper, etc.)
37 // so we default to testMode=false
38 var app *App
39 if scopes != nil {
40 app, err = NewAppWithScopes(baseURL, store, scopes)
41 } else {
42 app, err = NewApp(baseURL, store, "*", false)
43 }
44 if err != nil {
45 return nil, fmt.Errorf("failed to create OAuth app: %w", err)
46 }
47
48 // Channel to receive callback result
49 resultChan := make(chan *InteractiveResult, 1)
50 errorChan := make(chan error, 1)
51
52 // Create callback handler
53 callbackHandler := func(w http.ResponseWriter, r *http.Request) {
54 // Process callback
55 sessionData, err := app.ProcessCallback(r.Context(), r.URL.Query())
56 if err != nil {
57 errorChan <- fmt.Errorf("failed to process callback: %w", err)
58 http.Error(w, "OAuth callback failed", http.StatusInternalServerError)
59 return
60 }
61
62 // Resume session
63 session, err := app.ResumeSession(r.Context(), sessionData.AccountDID, sessionData.SessionID)
64 if err != nil {
65 errorChan <- fmt.Errorf("failed to resume session: %w", err)
66 http.Error(w, "Failed to resume session", http.StatusInternalServerError)
67 return
68 }
69
70 // Send result
71 resultChan <- &InteractiveResult{
72 SessionData: sessionData,
73 Session: session,
74 App: app,
75 }
76
77 // Return success to browser
78 w.Header().Set("Content-Type", "text/html")
79 fmt.Fprintf(w, "<html><body><h1>Authorization Successful!</h1><p>You can close this window and return to the terminal.</p></body></html>")
80 }
81
82 // Register callback handler
83 if err := registerCallback(callbackHandler); err != nil {
84 return nil, fmt.Errorf("failed to register callback: %w", err)
85 }
86
87 // Start auth flow
88 authURL, err := app.StartAuthFlow(ctx, handle)
89 if err != nil {
90 return nil, fmt.Errorf("failed to start auth flow: %w", err)
91 }
92
93 // Display auth URL
94 if err := displayAuthURL(authURL); err != nil {
95 return nil, fmt.Errorf("failed to display auth URL: %w", err)
96 }
97
98 // Wait for callback result
99 select {
100 case result := <-resultChan:
101 return result, nil
102 case err := <-errorChan:
103 return nil, err
104 case <-time.After(5 * time.Minute):
105 return nil, fmt.Errorf("OAuth flow timed out after 5 minutes")
106 }
107}