github.com/defang-io/defang/src@v0.0.0-20240505002154-bdf411911834/pkg/github/auth.go (about) 1 package github 2 3 import ( 4 "context" 5 "fmt" 6 "html/template" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "strings" 11 12 "github.com/defang-io/defang/src/pkg/term" 13 "github.com/google/uuid" 14 "github.com/pkg/browser" 15 ) 16 17 const ( 18 authTemplateString = `<!DOCTYPE html> 19 <html> 20 <head> 21 <title>Defang | Authentication Status</title> 22 <style> 23 body { 24 font-family: 'Exo', sans-serif; 25 background: linear-gradient(to right, #1e3c72, #2a5298); 26 color: white; 27 display: flex; 28 justify-content: center; 29 align-items: center; 30 height: 100vh; 31 margin: 0; 32 } 33 .container { 34 text-align: center; 35 } 36 .status-message { 37 font-size: 2em; 38 margin-bottom: 1em; 39 } 40 .close-link { 41 font-size: 1em; 42 cursor: pointer; 43 } 44 </style> 45 </head> 46 <body> 47 <div class="container"> 48 <h1>Welcome to Defang</h1> 49 <p class="status-message">{{.StatusMessage}}</p> 50 <p class="close-link" onclick="window.close()">You can close this window.</p> 51 </div> 52 </body> 53 </html>` 54 ) 55 56 var ( 57 authTemplate = template.Must(template.New("auth").Parse(authTemplateString)) 58 ) 59 60 func StartAuthCodeFlow(ctx context.Context, clientId string) (string, error) { 61 ctx, cancel := context.WithCancel(ctx) 62 63 // Generate random state 64 state := uuid.NewString() 65 66 // Create a channel to wait for the server to finish 67 ch := make(chan string) 68 69 var authorizeUrl string 70 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 71 if r.URL.Path == "/" { 72 http.Redirect(w, r, authorizeUrl, http.StatusFound) 73 return 74 } 75 if r.URL.Path != "/auth" { 76 http.NotFound(w, r) 77 return 78 } 79 defer close(ch) 80 query := r.URL.Query() 81 if query.Get("state") != state { 82 http.Error(w, "invalid state", http.StatusBadRequest) 83 return 84 } 85 msg := "Authentication successful" 86 if query.Get("error") != "" { 87 msg = "Authentication failed: " + query.Get("error_description") 88 } 89 ch <- query.Get("code") 90 authTemplate.Execute(w, struct{ StatusMessage string }{msg}) 91 }) 92 93 server := httptest.NewServer(handler) 94 defer server.Close() 95 96 values := url.Values{ 97 "client_id": {clientId}, 98 "state": {state}, 99 "redirect_uri": {server.URL + "/auth"}, 100 "scope": {"read:org user:email"}, // required for membership check; space-delimited 101 // "login": {";TODO: from state file"}, 102 } 103 authorizeUrl = "https://github.com/login/oauth/authorize?" + values.Encode() 104 105 n, _ := fmt.Printf("Please visit %s and log in. (Right click the URL or press ENTER to open browser)\r", server.URL) 106 defer fmt.Print(strings.Repeat(" ", n), "\r") // TODO: use termenv to clear line 107 108 input := term.NewNonBlockingStdin() 109 defer input.Close() // abort the read 110 go func() { 111 var b [1]byte 112 for { 113 if _, err := input.Read(b[:]); err != nil { 114 return // exit goroutine 115 } 116 switch b[0] { 117 case 3: // Ctrl-C 118 cancel() 119 case 10, 13: // Enter or Return 120 browser.OpenURL(server.URL) 121 } 122 } 123 }() 124 125 select { 126 case <-ctx.Done(): 127 return "", ctx.Err() 128 case code := <-ch: 129 return code, nil 130 } 131 }