/
browser.go
141 lines (118 loc) · 4.25 KB
/
browser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/mitchellh/colorstring"
"github.com/skratchdot/open-golang/open"
"golang.org/x/oauth2"
)
// browserLogin implements an oauth2.TokenSource for interactive browser logins.
type browserLogin struct {
oauthConfig *oauth2.Config
}
// NewBrowserLogin will return an oauth2.TokenSource that will return a Token from an interactive browser login.
func NewBrowserLogin(oauthConfig *oauth2.Config) *browserLogin {
return &browserLogin{
oauthConfig: oauthConfig,
}
}
// Token will return an oauth2.Token retrieved from an interactive browser login.
func (b *browserLogin) Token() (*oauth2.Token, error) {
browser := &oauthBrowser{}
return browser.GetTokenFromBrowser(context.Background(), b.oauthConfig)
}
// oauthBrowser implements the Browser interface using the real OAuth2 login flow.
type oauthBrowser struct{}
// GetTokenFromBrowser opens a browser window for the user to log in and handles the OAuth2 flow to obtain a token.
func (b *oauthBrowser) GetTokenFromBrowser(ctx context.Context, conf *oauth2.Config) (*oauth2.Token, error) {
// Launch a request to Auth0's authorization endpoint.
colorstring.Printf("[bold][yellow]The default web browser has been opened at %s. Please continue the login in the web browser.\n", conf.Endpoint.AuthURL)
// Prepare the /authorize request with randomly generated state, offline access option, and audience
aud := "https://api.hashicorp.cloud"
opt := oauth2.SetAuthURLParam("audience", aud)
authzURL := conf.AuthCodeURL(generateRandomString(32), oauth2.AccessTypeOffline, opt)
// Handle ctrl-c while waiting for the callback
sigintCh := make(chan os.Signal, 1)
signal.Notify(sigintCh, os.Interrupt)
defer signal.Stop(sigintCh)
if err := open.Start(authzURL); err != nil {
return nil, fmt.Errorf("failed to open browser at URL %q: %w", authzURL, err)
}
// Start callback server
callbackEndpoint := &callbackEndpoint{}
callbackEndpoint.shutdownSignal = make(chan error)
server := &http.Server{
Addr: ":8443",
Handler: nil,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
callbackEndpoint.server = server
http.Handle("/oidc/callback", callbackEndpoint)
go func() {
err := server.ListenAndServe()
if err != nil {
callbackEndpoint.shutdownSignal <- fmt.Errorf("failed to start callback server: %w", err)
}
}()
// Wait for either the callback to finish, SIGINT to be received or up to 2 minutes
select {
case err := <-callbackEndpoint.shutdownSignal:
if err != nil {
return nil, err
}
err = callbackEndpoint.server.Shutdown(context.Background())
if err != nil {
return nil, fmt.Errorf("failed to shutdown callback server: %w", err)
}
// Exchange the code returned in the callback for a token.
tok, err := conf.Exchange(ctx, callbackEndpoint.code)
if err != nil {
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
}
return tok, nil
case <-sigintCh:
return nil, errors.New("interrupted")
case <-time.After(2 * time.Minute):
return nil, errors.New("timed out waiting for response from provider")
}
}
// callbackEndpoint exposes the confiugration for the callback server.
type callbackEndpoint struct {
server *http.Server
code string
shutdownSignal chan error
}
// callbackEndpoint endpoint ServeHTTP confirms if an Authorization code was received from Auth0.
func (h *callbackEndpoint) ServeHTTP(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
if code != "" {
h.code = code
fmt.Fprintln(w, "Login is successful. You may close the browser and return to the command line.")
colorstring.Println("[bold][green]Success!")
} else {
fmt.Fprintln(w, "Login is not successful. You may close the browser and try again.")
}
h.shutdownSignal <- nil
}
// generateRandomString returns a URL-safe, base64 encoded
// securely generated random string.
func generateRandomString(n int) string {
b := make([]byte, n)
_, err := rand.Read(b)
// Note that err == nil only if we read len(b) bytes.
if err != nil {
panic(err)
}
return base64.RawURLEncoding.EncodeToString(b)
}