@@ -2,17 +2,10 @@ package login
22
33import (
44 "context"
5- "crypto/rand"
6- "crypto/sha256"
7- "embed"
8- "encoding/base64"
9- "encoding/json"
105 "fmt"
11- "io"
126 "net"
137 "net/http"
148 "net/url"
15- "os/exec"
169 rt "runtime"
1710 "strings"
1811 "time"
@@ -24,28 +17,19 @@ import (
2417 "github.com/smartcontractkit/cre-cli/internal/constants"
2518 "github.com/smartcontractkit/cre-cli/internal/credentials"
2619 "github.com/smartcontractkit/cre-cli/internal/environments"
20+ "github.com/smartcontractkit/cre-cli/internal/oauth"
2721 "github.com/smartcontractkit/cre-cli/internal/runtime"
2822 "github.com/smartcontractkit/cre-cli/internal/tenantctx"
2923 "github.com/smartcontractkit/cre-cli/internal/ui"
3024)
3125
3226var (
33- httpClient = & http.Client {Timeout : 10 * time .Second }
34- errorPage = "htmlPages/error.html"
35- successPage = "htmlPages/success.html"
36- waitingPage = "htmlPages/waiting.html"
37- stylePage = "htmlPages/output.css"
38-
3927 // OrgMembershipErrorSubstring is the error message substring returned by Auth0
4028 // when a user doesn't belong to any organization during the auth flow.
4129 // This typically happens during sign-up when the organization hasn't been created yet.
4230 OrgMembershipErrorSubstring = "user does not belong to any organization"
4331)
4432
45- //go:embed htmlPages/*.html
46- //go:embed htmlPages/*.css
47- var htmlFiles embed.FS
48-
4933func New (runtimeCtx * runtime.Context ) * cobra.Command {
5034 cmd := & cobra.Command {
5135 Use : "login" ,
@@ -102,7 +86,7 @@ func (h *handler) execute() error {
10286
10387 // Use spinner for the token exchange
10488 h .spinner .Start ("Exchanging authorization code..." )
105- tokenSet , err := h . exchangeCodeForTokens (context .Background (), code )
89+ tokenSet , err := oauth . ExchangeAuthorizationCode (context .Background (), nil , h . environmentSet , code , h . lastPKCEVerifier )
10690 if err != nil {
10791 h .spinner .StopAll ()
10892 h .log .Error ().Err (err ).Msg ("code exchange failed" )
@@ -162,13 +146,13 @@ func (h *handler) startAuthFlow() (string, error) {
162146 }
163147 }()
164148
165- verifier , challenge , err := generatePKCE ()
149+ verifier , challenge , err := oauth . GeneratePKCE ()
166150 if err != nil {
167151 h .spinner .Stop ()
168152 return "" , err
169153 }
170154 h .lastPKCEVerifier = verifier
171- h .lastState = randomState ()
155+ h .lastState = oauth . RandomState ()
172156
173157 authURL := h .buildAuthURL (challenge , h .lastState )
174158
@@ -180,7 +164,7 @@ func (h *handler) startAuthFlow() (string, error) {
180164 ui .URL (authURL )
181165 ui .Line ()
182166
183- if err := openBrowser (authURL , rt .GOOS ); err != nil {
167+ if err := oauth . OpenBrowser (authURL , rt .GOOS ); err != nil {
184168 ui .Warning ("Could not open browser automatically" )
185169 ui .Dim ("Please open the URL above in your browser" )
186170 ui .Line ()
@@ -199,19 +183,7 @@ func (h *handler) startAuthFlow() (string, error) {
199183}
200184
201185func (h * handler ) setupServer (codeCh chan string ) (* http.Server , net.Listener , error ) {
202- mux := http .NewServeMux ()
203- mux .HandleFunc ("/callback" , h .callbackHandler (codeCh ))
204-
205- // TODO: Add a fallback port in case the default port is in use
206- listener , err := net .Listen ("tcp" , constants .AuthListenAddr )
207- if err != nil {
208- return nil , nil , fmt .Errorf ("failed to listen on %s: %w" , constants .AuthListenAddr , err )
209- }
210-
211- return & http.Server {
212- Handler : mux ,
213- ReadHeaderTimeout : 5 * time .Second ,
214- }, listener , nil
186+ return oauth .NewCallbackHTTPServer (constants .AuthListenAddr , h .callbackHandler (codeCh ))
215187}
216188
217189func (h * handler ) callbackHandler (codeCh chan string ) http.HandlerFunc {
@@ -225,120 +197,52 @@ func (h *handler) callbackHandler(codeCh chan string) http.HandlerFunc {
225197 if strings .Contains (errorDesc , OrgMembershipErrorSubstring ) {
226198 if h .retryCount >= maxOrgNotFoundRetries {
227199 h .log .Error ().Int ("retries" , h .retryCount ).Msg ("organization setup timed out after maximum retries" )
228- h . serveEmbeddedHTML ( w , errorPage , http .StatusBadRequest )
200+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageError , http .StatusBadRequest )
229201 return
230202 }
231203
232204 // Generate new authentication credentials for the retry
233- verifier , challenge , err := generatePKCE ()
205+ verifier , challenge , err := oauth . GeneratePKCE ()
234206 if err != nil {
235207 h .log .Error ().Err (err ).Msg ("failed to prepare authentication retry" )
236- h . serveEmbeddedHTML ( w , errorPage , http .StatusInternalServerError )
208+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageError , http .StatusInternalServerError )
237209 return
238210 }
239211 h .lastPKCEVerifier = verifier
240- h .lastState = randomState ()
212+ h .lastState = oauth . RandomState ()
241213 h .retryCount ++
242214
243215 // Build the new auth URL for redirect
244216 authURL := h .buildAuthURL (challenge , h .lastState )
245217
246218 h .log .Debug ().Int ("attempt" , h .retryCount ).Int ("max" , maxOrgNotFoundRetries ).Msg ("organization setup in progress, retrying" )
247- h . serveWaitingPage ( w , authURL )
219+ oauth . ServeWaitingPage ( h . log , w , authURL )
248220 return
249221 }
250222
251223 // Generic Auth0 error
252224 h .log .Error ().Str ("error" , errorParam ).Str ("description" , errorDesc ).Msg ("auth error in callback" )
253- h . serveEmbeddedHTML ( w , errorPage , http .StatusBadRequest )
225+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageError , http .StatusBadRequest )
254226 return
255227 }
256228
257229 if st := r .URL .Query ().Get ("state" ); st == "" || h .lastState == "" || st != h .lastState {
258230 h .log .Error ().Msg ("invalid state in response" )
259- h . serveEmbeddedHTML ( w , errorPage , http .StatusBadRequest )
231+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageError , http .StatusBadRequest )
260232 return
261233 }
262234 code := r .URL .Query ().Get ("code" )
263235 if code == "" {
264236 h .log .Error ().Msg ("no code in response" )
265- h . serveEmbeddedHTML ( w , errorPage , http .StatusBadRequest )
237+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageError , http .StatusBadRequest )
266238 return
267239 }
268240
269- h . serveEmbeddedHTML ( w , successPage , http .StatusOK )
241+ oauth . ServeEmbeddedHTML ( h . log , w , oauth . PageSuccess , http .StatusOK )
270242 codeCh <- code
271243 }
272244}
273245
274- func (h * handler ) serveEmbeddedHTML (w http.ResponseWriter , filePath string , status int ) {
275- htmlContent , err := htmlFiles .ReadFile (filePath )
276- if err != nil {
277- h .log .Error ().Err (err ).Str ("file" , filePath ).Msg ("failed to read embedded HTML file" )
278- h .sendHTTPError (w )
279- return
280- }
281-
282- cssContent , err := htmlFiles .ReadFile (stylePage )
283- if err != nil {
284- h .log .Error ().Err (err ).Str ("file" , stylePage ).Msg ("failed to read embedded CSS file" )
285- h .sendHTTPError (w )
286- return
287- }
288-
289- modified := strings .Replace (
290- string (htmlContent ),
291- `<link rel="stylesheet" href="./output.css" />` ,
292- fmt .Sprintf ("<style>%s</style>" , string (cssContent )),
293- 1 ,
294- )
295-
296- w .Header ().Set ("Content-Type" , "text/html" )
297- w .WriteHeader (status )
298- if _ , err := w .Write ([]byte (modified )); err != nil {
299- h .log .Error ().Err (err ).Msg ("failed to write HTML response" )
300- }
301- }
302-
303- // serveWaitingPage serves the waiting page with the redirect URL injected.
304- // This is used when handling organization membership errors during sign-up flow.
305- func (h * handler ) serveWaitingPage (w http.ResponseWriter , redirectURL string ) {
306- htmlContent , err := htmlFiles .ReadFile (waitingPage )
307- if err != nil {
308- h .log .Error ().Err (err ).Str ("file" , waitingPage ).Msg ("failed to read waiting page HTML file" )
309- h .sendHTTPError (w )
310- return
311- }
312-
313- cssContent , err := htmlFiles .ReadFile (stylePage )
314- if err != nil {
315- h .log .Error ().Err (err ).Str ("file" , stylePage ).Msg ("failed to read embedded CSS file" )
316- h .sendHTTPError (w )
317- return
318- }
319-
320- // Inject CSS inline
321- modified := strings .Replace (
322- string (htmlContent ),
323- `<link rel="stylesheet" href="./output.css" />` ,
324- fmt .Sprintf ("<style>%s</style>" , string (cssContent )),
325- 1 ,
326- )
327-
328- // Inject the redirect URL
329- modified = strings .Replace (modified , "{{REDIRECT_URL}}" , redirectURL , 1 )
330-
331- w .Header ().Set ("Content-Type" , "text/html" )
332- w .WriteHeader (http .StatusOK )
333- if _ , err := w .Write ([]byte (modified )); err != nil {
334- h .log .Error ().Err (err ).Msg ("failed to write waiting page response" )
335- }
336- }
337-
338- func (h * handler ) sendHTTPError (w http.ResponseWriter ) {
339- http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
340- }
341-
342246func (h * handler ) buildAuthURL (codeChallenge , state string ) string {
343247 params := url.Values {}
344248 params .Set ("client_id" , h .environmentSet .ClientID )
@@ -355,41 +259,6 @@ func (h *handler) buildAuthURL(codeChallenge, state string) string {
355259 return h .environmentSet .AuthBase + constants .AuthAuthorizePath + "?" + params .Encode ()
356260}
357261
358- func (h * handler ) exchangeCodeForTokens (ctx context.Context , code string ) (* credentials.CreLoginTokenSet , error ) {
359- form := url.Values {}
360- form .Set ("grant_type" , "authorization_code" )
361- form .Set ("client_id" , h .environmentSet .ClientID )
362- form .Set ("code" , code )
363- form .Set ("redirect_uri" , constants .AuthRedirectURI )
364- form .Set ("code_verifier" , h .lastPKCEVerifier )
365-
366- req , err := http .NewRequestWithContext (ctx , http .MethodPost , h .environmentSet .AuthBase + constants .AuthTokenPath , strings .NewReader (form .Encode ()))
367- if err != nil {
368- return nil , fmt .Errorf ("create request: %w" , err )
369- }
370- req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
371-
372- resp , err := httpClient .Do (req ) // #nosec G704 -- URL is from trusted environment config
373- if err != nil {
374- return nil , fmt .Errorf ("perform request: %w" , err )
375- }
376- defer resp .Body .Close ()
377-
378- body , err := io .ReadAll (io .LimitReader (resp .Body , 1 << 20 ))
379- if err != nil {
380- return nil , fmt .Errorf ("read response: %w" , err )
381- }
382- if resp .StatusCode != http .StatusOK {
383- return nil , fmt .Errorf ("status %d: %s" , resp .StatusCode , body )
384- }
385-
386- var tokenSet credentials.CreLoginTokenSet
387- if err := json .Unmarshal (body , & tokenSet ); err != nil {
388- return nil , fmt .Errorf ("unmarshal token set: %w" , err )
389- }
390- return & tokenSet , nil
391- }
392-
393262func (h * handler ) fetchTenantConfig (tokenSet * credentials.CreLoginTokenSet ) error {
394263 creds := & credentials.Credentials {
395264 Tokens : tokenSet ,
@@ -404,35 +273,3 @@ func (h *handler) fetchTenantConfig(tokenSet *credentials.CreLoginTokenSet) erro
404273
405274 return tenantctx .FetchAndWriteContext (context .Background (), gqlClient , envName , h .log )
406275}
407-
408- func openBrowser (urlStr string , goos string ) error {
409- switch goos {
410- case "darwin" :
411- return exec .Command ("open" , urlStr ).Start ()
412- case "linux" :
413- return exec .Command ("xdg-open" , urlStr ).Start ()
414- case "windows" :
415- return exec .Command ("rundll32" , "url.dll,FileProtocolHandler" , urlStr ).Start ()
416- default :
417- return fmt .Errorf ("unsupported OS: %s" , goos )
418- }
419- }
420-
421- func generatePKCE () (verifier , challenge string , err error ) {
422- b := make ([]byte , 32 )
423- if _ , err = rand .Read (b ); err != nil {
424- return "" , "" , err
425- }
426- verifier = base64 .RawURLEncoding .EncodeToString (b )
427- sum := sha256 .Sum256 ([]byte (verifier ))
428- challenge = base64 .RawURLEncoding .EncodeToString (sum [:])
429- return verifier , challenge , nil
430- }
431-
432- func randomState () string {
433- b := make ([]byte , 16 )
434- if _ , err := rand .Read (b ); err != nil {
435- return fmt .Sprintf ("%d" , time .Now ().UnixNano ())
436- }
437- return base64 .RawURLEncoding .EncodeToString (b )
438- }
0 commit comments