-
Notifications
You must be signed in to change notification settings - Fork 85
Expand file tree
/
Copy pathwebapp_flow.go
More file actions
151 lines (128 loc) · 3.7 KB
/
webapp_flow.go
File metadata and controls
151 lines (128 loc) · 3.7 KB
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
142
143
144
145
146
147
148
149
150
151
// Package webapp implements the OAuth Web Application authorization flow for client applications by
// starting a server at localhost to receive the web redirect after the user has authorized the application.
package webapp
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"strconv"
"github.com/cli/oauth/api"
)
type httpClient interface {
PostForm(string, url.Values) (*http.Response, error)
}
// Flow holds the state for the steps of OAuth Web Application flow.
type Flow struct {
server *localServer
clientID string
state string
}
// InitFlow creates a new Flow instance by detecting a locally available port number.
func InitFlow() (*Flow, error) {
portStr := os.Getenv("GH_OAUTH_PORT")
port := 0
if portStr != "" {
var err error
port, err = strconv.Atoi(portStr)
if err != nil {
port = 0
}
}
server, err := bindLocalServerWithPort(port)
if err != nil {
return nil, err
}
state, _ := randomString(20)
return &Flow{
server: server,
state: state,
}, nil
}
// BrowserParams are GET query parameters for initiating the web flow.
type BrowserParams struct {
ClientID string
RedirectURI string
Scopes []string
Audience string
LoginHandle string
AllowSignup bool
}
// BrowserURL appends GET query parameters to baseURL and returns the url that the user should
// navigate to in their web browser.
func (flow *Flow) BrowserURL(baseURL string, params BrowserParams) (string, error) {
ru, err := url.Parse(params.RedirectURI)
if err != nil {
return "", err
}
ru.Host = fmt.Sprintf("%s:%d", ru.Hostname(), flow.server.Port())
flow.server.CallbackPath = ru.Path
flow.clientID = params.ClientID
q := url.Values{}
q.Set("client_id", params.ClientID)
q.Set("redirect_uri", ru.String())
q.Set("scope", strings.Join(params.Scopes, " "))
q.Set("state", flow.state)
if params.Audience != "" {
q.Set("audience", params.Audience)
}
if params.LoginHandle != "" {
q.Set("login", params.LoginHandle)
}
if !params.AllowSignup {
q.Set("allow_signup", "false")
}
return fmt.Sprintf("%s?%s", baseURL, q.Encode()), nil
}
// StartServer starts the localhost server and blocks until it has received the web redirect. The
// writeSuccess function can be used to render a HTML page to the user upon completion.
func (flow *Flow) StartServer(writeSuccess func(io.Writer)) error {
flow.server.WriteSuccessHTML = writeSuccess
return flow.server.Serve()
}
// AccessToken blocks until the browser flow has completed and returns the access token.
//
// Deprecated: use Wait.
func (flow *Flow) AccessToken(c httpClient, tokenURL, clientSecret string) (*api.AccessToken, error) {
return flow.Wait(context.Background(), c, tokenURL, WaitOptions{ClientSecret: clientSecret})
}
// WaitOptions specifies parameters to exchange the access token for.
type WaitOptions struct {
// ClientSecret is the app client secret value.
ClientSecret string
}
// Wait blocks until the browser flow has completed and returns the access token.
func (flow *Flow) Wait(ctx context.Context, c httpClient, tokenURL string, opts WaitOptions) (*api.AccessToken, error) {
code, err := flow.server.WaitForCode(ctx)
if err != nil {
return nil, err
}
if code.State != flow.state {
return nil, errors.New("state mismatch")
}
resp, err := api.PostForm(c, tokenURL,
url.Values{
"client_id": {flow.clientID},
"client_secret": {opts.ClientSecret},
"code": {code.Code},
"state": {flow.state},
})
if err != nil {
return nil, err
}
return resp.AccessToken()
}
func randomString(length int) (string, error) {
b := make([]byte, length/2)
_, err := rand.Read(b)
if err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}