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  }