/
login.go
216 lines (186 loc) · 6.51 KB
/
login.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
// Copyright 2016 Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
Package auth implements the logic required to authenticate the user and
generate access tokens for use with GCR.
*/
package auth
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
"os"
"strings"
"github.com/GoogleCloudPlatform/docker-credential-gcr/config"
"github.com/toqueteos/webbrowser"
"golang.org/x/oauth2"
)
const redirectURIAuthCodeInTitleBar = "urn:ietf:wg:oauth:2.0:oob"
// GCRLoginAgent implements the OAuth2 login dance, generating an Oauth2 access_token
// for the user. If AllowBrowser is set to true, the agent will attempt to
// obtain an authorization_code automatically by executing OpenBrowser and
// reading the redirect performed after a successful login. Otherwise, it will
// attempt to use In and Out to direct the user to the login portal and receive
// the authorization_code in response.
type GCRLoginAgent struct {
// Whether to execute OpenBrowser when authenticating the user.
AllowBrowser bool
// Read input from here; if nil, uses os.Stdin.
In io.Reader
// Write output to here; if nil, uses os.Stdout.
Out io.Writer
// Open the browser for the given url. If nil, uses webbrowser.Open.
OpenBrowser func(url string) error
}
// populate missing fields as described in the struct definition comments
func (a *GCRLoginAgent) init() {
if a.In == nil {
a.In = os.Stdin
}
if a.Out == nil {
a.Out = os.Stdout
}
if a.OpenBrowser == nil {
a.OpenBrowser = webbrowser.Open
}
}
// PerformLogin performs the auth dance necessary to obtain an
// authorization_code from the user and exchange it for an Oauth2 access_token.
func (a *GCRLoginAgent) PerformLogin() (*oauth2.Token, error) {
a.init()
conf := &oauth2.Config{
ClientID: config.GCRCredHelperClientID,
ClientSecret: config.GCRCredHelperClientNotSoSecret,
Scopes: config.GCRScopes,
Endpoint: config.GCROAuth2Endpoint,
}
verifier, challenge, method, err := codeChallengeParams()
authCodeOpts := []oauth2.AuthCodeOption{
oauth2.AccessTypeOffline,
oauth2.SetAuthURLParam("code_challenge", challenge),
oauth2.SetAuthURLParam("code_challenge_method", method),
}
if a.AllowBrowser {
// Attempt to receive the authorization code via redirect URL
if ln, port, err := getListener(); err == nil {
defer ln.Close()
// open a web browser and listen on the redirect URL port
conf.RedirectURL = fmt.Sprintf("http://localhost:%d", port)
url := conf.AuthCodeURL("state", authCodeOpts...)
if err := a.OpenBrowser(url); err == nil {
if code, err := handleCodeResponse(ln); err == nil {
return conf.Exchange(
config.OAuthHTTPContext,
code,
oauth2.SetAuthURLParam("code_verifier", verifier))
}
}
}
}
// If we can't or shouldn't automatically retrieve the code via browser,
// default to a command line prompt.
code, err := a.codeViaPrompt(conf, authCodeOpts)
if err != nil {
return nil, err
}
return conf.Exchange(
config.OAuthHTTPContext,
code,
oauth2.SetAuthURLParam("code_verifier", verifier))
}
func (a *GCRLoginAgent) codeViaPrompt(conf *oauth2.Config, authCodeOpts []oauth2.AuthCodeOption) (string, error) {
// Direct the user to our login portal
conf.RedirectURL = redirectURIAuthCodeInTitleBar
url := conf.AuthCodeURL("state", authCodeOpts...)
fmt.Fprintln(a.Out, "Please visit the following URL and complete the authorization dialog:")
fmt.Fprintf(a.Out, "%v\n", url)
// Receive the authorization_code in response
fmt.Fprintln(a.Out, "Authorization code:")
var code string
if _, err := fmt.Fscan(a.In, &code); err != nil {
return "", err
}
return code, nil
}
func getListener() (net.Listener, int, error) {
laddr := net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0} // port: 0 == find free port
ln, err := net.ListenTCP("tcp4", &laddr)
if err != nil {
return nil, 0, err
}
return ln, ln.Addr().(*net.TCPAddr).Port, nil
}
func handleCodeResponse(ln net.Listener) (string, error) {
conn, err := ln.Accept()
if err != nil {
return "", err
}
srvConn := httputil.NewServerConn(conn, nil)
defer srvConn.Close()
req, err := srvConn.Read()
if err != nil {
return "", err
}
code := req.URL.Query().Get("code")
resp := &http.Response{
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Close: true,
ContentLength: -1, // designates unknown length
}
defer srvConn.Write(req, resp)
// If the code couldn't be obtained, inform the user via the browser and
// return an error.
// TODO i18n?
if code == "" {
err := fmt.Errorf("Code not present in response: %s", req.URL.String())
resp.Body = getResponseBody("ERROR: Authentication code not present in response, please retry with --no-browser.")
return "", err
}
resp.Body = getResponseBody("Success! You may now close your browser.")
return code, nil
}
// turn a string into an io.ReadCloser as required by an http.Response
func getResponseBody(body string) io.ReadCloser {
reader := strings.NewReader(body)
return ioutil.NopCloser(reader)
}
// generates the values used in "Proof Key for Code Exchange by OAuth Public Clients"
// https://tools.ietf.org/html/rfc7636
// https://developers.google.com/identity/protocols/OAuth2InstalledApp#step1-code-verifier
func codeChallengeParams() (verifier, challenge, method string, err error) {
// A `code_verifier` is a high-entropy cryptographic random string using the unreserved characters
// [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
// with a minimum length of 43 characters and a maximum length of 128 characters.
b := make([]byte, 32)
_, err = rand.Read(b)
if err != nil {
return "", "", "", err
}
verifier = base64.RawURLEncoding.EncodeToString(b)
// https://tools.ietf.org/html/rfc7636#section-4.2
// If the client is capable of using "S256", it MUST use "S256":
// code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
sha := sha256.Sum256([]byte(verifier))
challenge = base64.RawURLEncoding.EncodeToString(sha[:])
return verifier, challenge, "S256", nil
}