@@ -4,44 +4,28 @@ import (
44 "fmt"
55 "os/exec"
66 "runtime"
7- "strings"
8-
9- "github.com/AlecAivazis/survey/v2"
107
118 "github.com/algolia/cli/api/dashboard"
129 "github.com/algolia/cli/pkg/iostreams"
13- "github.com/algolia/cli/pkg/prompt"
1410)
1511
16- // OAuthOptions configures the behaviour of RunOAuthFlow.
17- type OAuthOptions struct {
18- // Code, if non-empty, skips the interactive browser+prompt step and
19- // exchanges this authorization code directly. The caller must also
20- // supply CodeVerifier so the PKCE handshake succeeds.
21- Code string
22- CodeVerifier string
23- }
24-
25- // RunInteractiveOAuth runs the browser-based OAuth PKCE flow and returns
26- // a valid access token. This is the authentication-only portion — it does
27- // not handle application selection or profile setup.
28- // If signup is true, the browser opens to the sign-up page.
29- func RunInteractiveOAuth (io * iostreams.IOStreams , client * dashboard.Client , signup bool , opts * OAuthOptions ) (string , error ) {
30- if opts == nil {
31- opts = & OAuthOptions {}
32- }
33-
34- // Fast path: caller already has an authorization code + verifier.
35- if opts .Code != "" {
36- return exchangeCode (io , client , opts .Code , opts .CodeVerifier )
37- }
12+ // RunOAuth runs the OAuth PKCE flow with a local callback server and returns
13+ // a valid access token. A local HTTP server is started on a random port to
14+ // receive the authorization code via redirect — no copy-paste required.
15+ //
16+ // When openBrowser is true the authorize URL is opened automatically;
17+ // otherwise only the URL is printed (useful when the browser can't be
18+ // launched, e.g. SSH / containers).
19+ //
20+ // If signup is true the browser opens to the sign-up page.
21+ func RunOAuth (io * iostreams.IOStreams , client * dashboard.Client , signup , openBrowser bool ) (string , error ) {
22+ cs := io .ColorScheme ()
3823
39- if ! io .CanPrompt () {
40- return "" , fmt .Errorf ("not logged in — run `algolia auth login` interactively first, or use --print-url and --code for non-interactive mode" )
24+ redirectURI , resultCh , err := StartCallbackServer ()
25+ if err != nil {
26+ return "" , err
4127 }
4228
43- cs := io .ColorScheme ()
44-
4529 codeVerifier , err := GenerateCodeVerifier ()
4630 if err != nil {
4731 return "" , fmt .Errorf ("failed to generate PKCE verifier: %w" , err )
@@ -50,58 +34,35 @@ func RunInteractiveOAuth(io *iostreams.IOStreams, client *dashboard.Client, sign
5034
5135 var authorizeURL string
5236 if signup {
53- authorizeURL = client .SignupAuthorizeURL (codeChallenge )
54- fmt .Fprintf (io .Out , "Opening browser to create an account...\n " )
37+ authorizeURL = client .SignupAuthorizeURL (codeChallenge , redirectURI )
5538 } else {
56- authorizeURL = client .AuthorizeURL (codeChallenge )
57- fmt .Fprintf (io .Out , "Opening browser to sign in...\n " )
39+ authorizeURL = client .AuthorizeURL (codeChallenge , redirectURI )
5840 }
59- fmt .Fprintf (io .Out , "If the browser doesn't open, visit:\n %s\n \n " , cs .Bold (authorizeURL ))
60- _ = OpenBrowser (authorizeURL )
6141
62- var code string
63- err = prompt .SurveyAskOne (
64- & survey.Input {Message : "Paste the authorization code:" },
65- & code ,
66- survey .WithValidator (survey .Required ),
67- )
68- if err != nil {
69- return "" , err
42+ if openBrowser {
43+ if signup {
44+ fmt .Fprintf (io .Out , "Opening browser to create an account...\n " )
45+ } else {
46+ fmt .Fprintf (io .Out , "Opening browser to sign in...\n " )
47+ }
48+ fmt .Fprintf (io .Out , "If the browser doesn't open, visit:\n %s\n \n " , cs .Bold (authorizeURL ))
49+ _ = OpenBrowser (authorizeURL )
50+ } else {
51+ fmt .Fprintf (io .Out , "Open this URL in your browser to authenticate:\n \n %s\n \n " , cs .Bold (authorizeURL ))
7052 }
71- code = strings .TrimSpace (code )
7253
73- return exchangeCode (io , client , code , codeVerifier )
74- }
75-
76- // PrintAuthorizeURL generates a PKCE challenge, prints the authorize URL,
77- // and returns the code verifier so the caller can later exchange the code
78- // in a separate invocation via --code / --code-verifier.
79- func PrintAuthorizeURL (io * iostreams.IOStreams , client * dashboard.Client , signup bool ) (codeVerifier string , err error ) {
80- cs := io .ColorScheme ()
54+ fmt .Fprintf (io .Out , "Waiting for authentication...\n " )
55+ cbResult := <- resultCh
8156
82- codeVerifier , err = GenerateCodeVerifier ()
83- if err != nil {
84- return "" , fmt .Errorf ("failed to generate PKCE verifier: %w" , err )
57+ if cbResult .Error != "" {
58+ return "" , fmt .Errorf ("authorization failed: %s" , cbResult .Error )
8559 }
86- codeChallenge := CodeChallenge (codeVerifier )
87-
88- var authorizeURL string
89- if signup {
90- authorizeURL = client .SignupAuthorizeURL (codeChallenge )
91- } else {
92- authorizeURL = client .AuthorizeURL (codeChallenge )
60+ if cbResult .Code == "" {
61+ return "" , fmt .Errorf ("no authorization code received" )
9362 }
9463
95- fmt .Fprintf (io .Out , "Open this URL in your browser to authenticate:\n \n %s\n \n " , cs .Bold (authorizeURL ))
96- fmt .Fprintf (io .Out , "Then run:\n \n algolia auth login --code <AUTHORIZATION_CODE> --code-verifier %s\n \n " , codeVerifier )
97- return codeVerifier , nil
98- }
99-
100- func exchangeCode (io * iostreams.IOStreams , client * dashboard.Client , code , codeVerifier string ) (string , error ) {
101- cs := io .ColorScheme ()
102-
10364 io .StartProgressIndicatorWithLabel ("Exchanging code for tokens" )
104- tokenResp , err := client .AuthorizationCodeGrant (code , codeVerifier )
65+ tokenResp , err := client .AuthorizationCodeGrant (cbResult . Code , codeVerifier , redirectURI )
10566 io .StopProgressIndicator ()
10667 if err != nil {
10768 return "" , err
0 commit comments