-
Notifications
You must be signed in to change notification settings - Fork 0
/
auth.go
413 lines (362 loc) · 12.7 KB
/
auth.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
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
package gotd
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/user"
"path/filepath"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/net/context"
"golang.org/x/oauth2"
)
// This variable indicates whether the script should launch a web server to
// initiate the authorization flow or just display the URL in the terminal
// window. Note the following instructions based on this setting:
// * launchWebServer = true
// 1. Use OAuth2 credentials for a web application
// 2. Define authorized redirect URIs for the credential in the Google APIs
// Console and set the RedirectURL property on the config object to one
// of those redirect URIs. For example:
// config.RedirectURL = "http://localhost:8090"
// 3. In the startWebServer function below, update the URL in this line
// to match the redirect URI you selected:
// listener, err := net.Listen("tcp", "localhost:8090")
// The redirect URI identifies the URI to which the user is sent after
// completing the authorization flow. The listener then captures the
// authorization code in the URL and passes it back to this script.
// * launchWebServer = false
// 1. Use OAuth2 credentials for an installed application. (When choosing
// the application type for the OAuth2 client ID, select "Other".)
// 2. Set the redirect URI to "urn:ietf:wg:oauth:2.0:oob", like this:
// config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
// 3. When running the script, complete the auth flow. Then copy the
// authorization code from the browser and enter it on the command line.
const launchWebServer = true
const missingClientSecretsMessage = `
Please configure OAuth 2.0
To make this sample run, you need to populate the client_secrets.json file
found at:
%v
with information from the {{ Google Cloud Console }}{{ https://cloud.google.com/console }}For more information about the client_secrets.json file format, please visit:
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
`
// Auth for oauth2
type Auth struct {
redirectURL *url.URL
config *oauth2.Config
useTLS bool
tlsCert, tlsKey string
}
// NewAuth create Auth
func NewAuth() *Auth {
a := &Auth{}
return a
}
// SetTLS when redirectURL is TLS, it should be setted
func (a *Auth) SetTLS(TLSCertPath, TLSKeyPath string) {
a.tlsCert = TLSCertPath
a.tlsKey = TLSKeyPath
a.useTLS = true
}
// GetClient generate a Client. It returns the generated Client.
// clientsecret file format reference to client_secret.json.sample
func (a *Auth) GetClient(clientsecretPath, tokenFile string) *http.Client {
ctx := context.Background()
b, err := ioutil.ReadFile(clientsecretPath)
if err != nil {
log.Fatalf("Unable to read client secret file: %v", err)
}
// If modifying the scope, delete your previously saved credentials
// at ~/.credentials/youtube-go.json
err = a.configFromJSON(b)
if err != nil {
log.Fatalf("Unable to parse client secret file to config: %v", err)
}
// Use a redirect URI like this for a web app. The redirect URI must be a
// valid one for your OAuth2 credentials.
a.config.RedirectURL = a.redirectURL.String()
// Use the following redirect URI if launchWebServer=false in oauth2.go
// config.RedirectURL = "urn:ietf:wg:oauth:2.0:oob"
cacheFile, err := a.tokenCacheFile(tokenFile)
if err != nil {
log.Fatalf("Unable to get path to cached credential file. %v", err)
}
token, err := a.tokenFromFile(cacheFile)
if err != nil {
if launchWebServer {
fmt.Println("Trying to get token from web")
token, err = a.getTokenFromWeb()
} else {
fmt.Println("Trying to get token from prompt")
token, err = a.getTokenFromPrompt()
}
if err != nil {
log.Fatalf("Unable get token. %v", err)
}
a.saveToken(cacheFile, token)
}
// token.Expiry is the expiry of Access Token
// oauth2 does not change the token.Expiry even it refresh Access Token
// so we can use it to estimate the expiry of Refresh Token
// A Refresh Token is valid for 90 days
// Update Refresh Token ten days before the expiry date
Day := time.Hour * 24
valid_duration := Day * 90
refresh_token_expired := token.Expiry.Round(0).Add(valid_duration - Day*10).Before(time.Now())
if refresh_token_expired {
call := postAccessToken(ctx, http.DefaultClient, TokenGrantTypeRefreshToken, a.config.ClientID)
call = call.RefreshToken(token.RefreshToken)
call = call.AccessType(TokenAccessTypeOffline)
token, err = call.Do()
if err != nil {
log.Fatalf("Unable update refresh_token. %v", err)
}
a.saveToken(cacheFile, token)
}
client := a.config.Client(ctx, token)
return client
}
func (a *Auth) configFromJSON(jsonKey []byte, scope ...string) error {
type cred struct {
ClientID string `json:"client_id,omitempty"`
ClientSecret string `json:"client_secret,omitempty"`
RedirectURIs []string `json:"redirect_uris,omitempty"`
AuthURI string `json:"auth_uri,omitempty"`
TokenURI string `json:"token_uri,omitempty"`
}
var j struct {
Web *cred `json:"web,omitempty"`
Installed *cred `json:"installed,omitempty"`
}
if err := json.Unmarshal(jsonKey, &j); err != nil {
return err
}
var c *cred
switch {
case j.Web != nil:
c = j.Web
case j.Installed != nil:
c = j.Installed
default:
return fmt.Errorf("oauth2: no credentials found")
}
if len(c.RedirectURIs) < 1 {
return errors.New("oauth2: missing redirect URL in the client_credentials.json")
}
for _, redirectURL := range c.RedirectURIs {
var err error
a.redirectURL, err = url.Parse(redirectURL)
if err == nil {
break
}
}
a.config = &oauth2.Config{
ClientID: c.ClientID,
ClientSecret: c.ClientSecret,
RedirectURL: c.RedirectURIs[0],
Scopes: scope,
Endpoint: oauth2.Endpoint{
AuthURL: c.AuthURI,
TokenURL: c.TokenURI,
},
}
return nil
}
// startWebServer starts a web server that listens on http://localhost:8080.
// The webserver waits for an oauth code in the three-legged auth flow.
func (a *Auth) startWebServer() (codeCh chan string, err error) {
listener, err := net.Listen("tcp", a.redirectURL.Host)
if err != nil {
return nil, errors.Wrapf(err, "net.Listen")
}
codeCh = make(chan string)
handFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
code := r.FormValue("code")
codeCh <- code // send code to OAuth flow
listener.Close()
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "Received code: %v\r\nYou can now safely close this browser window.", code)
})
if a.useTLS {
log.Printf("create TLS Server\n")
log.Printf("certPath: %s\n", a.tlsCert)
log.Printf("keyPath: %s\n", a.tlsKey)
go http.ServeTLS(listener, handFunc, a.tlsCert, a.tlsKey)
} else {
go http.Serve(listener, handFunc)
}
return codeCh, nil
}
// Exchange the authorization code for an access token
func (a *Auth) exchangeToken(code string) (*oauth2.Token, error) {
// set access_type offline for refresh token
token, err := a.config.Exchange(oauth2.NoContext, code, oauth2.AccessTypeOffline)
if err != nil {
log.Fatalf("Unable to retrieve token %v", err)
}
return token, nil
}
// getTokenFromPrompt uses Config to request a Token and prompts the user
// to enter the token on the command line. It returns the retrieved Token.
func (a *Auth) getTokenFromPrompt() (*oauth2.Token, error) {
authURL := a.config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
var code string
fmt.Printf("Go to the following link in your browser. After completing "+
"the authorization flow, enter the authorization code on the command "+
"line: \n%v\n", authURL)
if _, err := fmt.Scan(&code); err != nil {
log.Fatalf("Unable to read authorization code %v", err)
}
fmt.Println(authURL)
return a.exchangeToken(code)
}
// getTokenFromWeb uses Config to request a Token.
// It returns the retrieved Token.
func (a *Auth) getTokenFromWeb() (*oauth2.Token, error) {
codeCh, err := a.startWebServer()
if err != nil {
fmt.Printf("Unable to start a web server. %s", err)
return nil, errors.Wrapf(err, "a.startWebServer")
}
authURL := a.config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
err = openURL(authURL)
if err != nil {
log.Fatalf("Unable to open authorization URL in web server: %v", err)
} else {
fmt.Println("Your browser has been opened to an authorization URL.",
"This program will resume once authorization has been provided.")
fmt.Println(authURL)
}
// Wait for the web server to get the code.
code := <-codeCh
return a.exchangeToken(code)
}
// tokenCacheFile generates credential file path/filename.
// It returns the generated credential path/filename.
func (a *Auth) tokenCacheFile(fileName string) (string, error) {
usr, err := user.Current()
if err != nil {
return "", err
}
tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
os.MkdirAll(tokenCacheDir, 0700)
return filepath.Join(tokenCacheDir,
url.QueryEscape(fileName)), err
}
// tokenFromFile retrieves a Token from a given file path.
// It returns the retrieved Token and any read error encountered.
func (a *Auth) tokenFromFile(file string) (*oauth2.Token, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
t := &oauth2.Token{}
err = json.NewDecoder(f).Decode(t)
defer f.Close()
return t, err
}
// saveToken uses a file path to create a file and store the
// token in it.
func (a *Auth) saveToken(file string, token *oauth2.Token) {
fmt.Println("trying to save token")
fmt.Printf("Saving credential file to: %s\n", file)
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Unable to cache oauth token: %v", err)
}
defer f.Close()
json.NewEncoder(f).Encode(token)
}
// postAccessToken https://developer.tdameritrade.com/authentication/apis/post/token-0
// The token endpoint returns an access token along with an optional refresh token
func postAccessToken(ctx context.Context, client *http.Client, grant_type, client_id string) *postAccessTokenCall {
c := &postAccessTokenCall{
ctx: ctx,
client: client,
body: url.Values{
"grant_type": []string{grant_type},
"client_id": []string{client_id},
},
}
return c
}
// postAccessTokenCall https://developer.tdameritrade.com/authentication/apis/post/token-0
// The token endpoint returns an access token along with an optional refresh token
type postAccessTokenCall struct {
ctx context.Context
// 不能用 oauth2 產生的,因會放在 Header 導致忽略 body 中的參數
// 建議使用 http.DefaultClient
client *http.Client
body url.Values
}
// RefreshToken https://developer.tdameritrade.com/authentication/apis/post/token-0
// Required if using refresh token grant
func (c *postAccessTokenCall) RefreshToken(refreshToken string) *postAccessTokenCall {
c.body.Set("refresh_token", refreshToken)
return c
}
// AccessType https://developer.tdameritrade.com/authentication/apis/post/token-0
// Set to offline to receive a refresh token on an authorization_code grant type request. Do not set to offline on a refresh_token grant type request.
func (c *postAccessTokenCall) AccessType(accessType string) *postAccessTokenCall {
c.body.Set("access_type", accessType)
return c
}
// Code https://developer.tdameritrade.com/authentication/apis/post/token-0
// Required if trying to use authorization code grant
func (c *postAccessTokenCall) Code(code string) *postAccessTokenCall {
c.body.Set("code", code)
return c
}
// RedirectURI https://developer.tdameritrade.com/authentication/apis/post/token-0
// Required if trying to use authorization code grant
func (c *postAccessTokenCall) RedirectURI(redirectURI string) *postAccessTokenCall {
c.body.Set("redirect_uri", redirectURI)
return c
}
func (c *postAccessTokenCall) doRequest() (*http.Response, error) {
reqHeaders := make(http.Header)
reqHeaders.Set("User-Agent", UserAgent)
reqHeaders.Set("Content-Type", "application/x-www-form-urlencoded")
body := strings.NewReader(c.body.Encode())
urls := ResolveRelative(basePath, "oauth2", "token")
req, err := http.NewRequest("POST", urls, body)
if err != nil {
return nil, errors.Wrapf(err, "http.NewRequest")
}
req.Header = reqHeaders
return SendRequest(c.ctx, c.client, req)
}
// Do send request
func (c *postAccessTokenCall) Do() (*oauth2.Token, error) {
res, err := c.doRequest()
if res != nil && res.StatusCode == http.StatusNotModified {
if res.Body != nil {
res.Body.Close()
}
return nil, &Error{
Code: res.StatusCode,
Header: res.Header,
}
}
if err != nil {
return nil, errors.Wrapf(err, "doRequest")
}
defer res.Body.Close()
if err := CheckResponse(res); err != nil {
return nil, errors.Wrapf(err, "CheckResponse")
}
token := &oauth2.Token{}
err = json.NewDecoder(res.Body).Decode(token)
token.Expiry = time.Now().Add(time.Second * 1800)
if err != nil {
return nil, errors.Wrapf(err, "json.NewDecoder")
}
return token, nil
}