-
Notifications
You must be signed in to change notification settings - Fork 0
/
gclientauth.go
218 lines (199 loc) · 6.63 KB
/
gclientauth.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
// Package gclientauth handles settings up a oAuth2 client and access token to
// talk to Google APIs from cli applications. The client libraries/APIs
// themselves don't handle authentication so it is necessary to first get the
// access token that is used to setup the API client to the APIs which this
// package handles.
//
// It wraps around golang.org/x/oauth2/google to provide some user
// friendly behavior and removes some boiler plate code for developers.
//
// The code is a mixture of various public Google tutorials and examples such
// as http://developers.google.com/youtube/v3/quickstart/go.
//
// In order to use this package:
//
// 1. Create a new project on the Google API Console
// (https://console.developers.google.com/).
//
// 2. In the new project, enable the Google APIs to access.
//
// 3. Setup up the credentials and download the client secret JSON
// configuration from https://console.developers.google.com/apis/credentials
//
// TIP:
//
// If it is **desktop/other** credential is chosen then gclientauth will show
// the user an URL to visit in order toget a code that can be used to get an
// access token..
//
// If it is a **web application** then gclientauth will attempt to run a local
// webserver to get the code itself and create a token so the user don't have to
// do anything themselves. Make sure that the credential's redirect url port
// matches what is passed to the package (e.g. localhost:8080).
//
//
// Example Usage:
//
// package main
//
// func main() {
// scopes := []string{youtube.YoutubeReadonlyScope}
// ctx := oauth2.NoContext
// token, config, err := gclientauth.GetGoogleOauth2Token(ctx, "client_secret.json", "accesstoken.json", scopes, false, "8080")
// ...
// cfg := config.Client(ctx, token)
// ...
// gp, err := googlephotos.New(cfg)
// ...
// res, err := gp.Albums.List().Do()
// for _, a := range res.Albums {
// fmt.Printf("%v\n", a.Title)
// }
// }
package gclientauth // import "lazyhacker.dev/gclientauth"
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"runtime"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// openURL opens a browser window to the specified location.
// This code originally appeared at:
// http://stackoverflow.com/questions/10377243/how-can-i-launch-a-process-that-is-not-a-file-in-go
func openURL(url string) error {
var cmd *exec.Cmd
switch runtime.GOOS {
case "linux":
cmd = exec.Command("xdg-open", url)
case "windows":
cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", "http://localhost:4001/")
case "darwin":
cmd = exec.Command("open", url)
default:
return fmt.Errorf("Cannot open URL %s on this platform", url)
}
return cmd.Run()
}
// getCodeFromInstalled asks the user to input the code from the auth URL.
func getCodeFromInstalled(url string, browser bool) string {
var code string
var berr error
if browser {
berr = openURL(url)
}
if berr != nil || !browser {
fmt.Printf("Visit the URL for the auth dialog: \n\t%v\n", url)
}
fmt.Print("Enter code: ")
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
code = scanner.Text()
break
}
return code
}
// getCodeFromWeb returns a code that is used to exchange for a token.
func getCodeFromWeb(config *oauth2.Config, authURL, port string) string {
hostname, err := url.Parse(config.RedirectURL)
if err != nil {
fmt.Errorf("Unable to determine the hostname from %v. %v", config.RedirectURL, err)
return ""
}
codeCh, err := startWebServer(hostname.Hostname(), port)
if err != nil {
log.Printf("Unable to start a web server. %v", err)
return ""
}
err = openURL(authURL)
if err != nil {
fmt.Errorf("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.\n")
fmt.Println(authURL)
}
// Wait for the web server to get the code.
code := <-codeCh
return code
}
// startWebServer starts a web server that waits for an oauth code in the
// three-legged auth flow.
func startWebServer(hostname, port string) (codeCh chan string, err error) {
listener, err := net.Listen("tcp", fmt.Sprintf("%v:%v", hostname, port))
if err != nil {
log.Printf("Unable to do listener on %v. %v", hostname, err)
return nil, err
}
codeCh = make(chan string)
go http.Serve(listener, 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)
}))
return codeCh, nil
}
func GetGoogleOauth2Token(ctx context.Context, credential, cachedtoken string, scopes []string, browser bool, port string) (*oauth2.Token, *oauth2.Config, error) {
type cred struct {
}
var credtype struct {
Web *cred `json:"web"`
Installed *cred `json:"installed"`
}
data, err := ioutil.ReadFile(credential)
if err != nil {
return nil, nil, fmt.Errorf("unable to read client credential file (%v). %v", credential, err)
}
config, err := google.ConfigFromJSON(data, scopes...)
if err != nil {
return nil, nil, fmt.Errorf("error parsing credential file. %v", err)
}
var token *oauth2.Token
// Try to read the token from the cache file.
// If an error occurs, do the three-legged OAuth flow because
// the token is invalid or doesn't exist.
t, err := ioutil.ReadFile(cachedtoken)
if err == nil {
err = json.Unmarshal(t, &token)
}
if (err != nil) || !token.Valid() {
var code string
// Redirect user to Google's consent page to ask for permission
// for the scopes specified above.
url := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
if err := json.Unmarshal(data, &credtype); err != nil {
return nil, nil, fmt.Errorf("error parsing credential file. %v", err)
}
switch {
case credtype.Installed != nil:
code = getCodeFromInstalled(url, browser)
case credtype.Web != nil:
code = getCodeFromWeb(config, url, port)
}
// Exchanging for a token invalidates previous code so the same
// code can't be used again.
token, err = config.Exchange(ctx, code)
if err != nil {
return nil, nil, fmt.Errorf("unable to get valid token. code = \"%v\"\n%v", code, err)
}
data, err := json.Marshal(token)
if err != nil {
return nil, nil, fmt.Errorf("unable to encode the token for writing to cache. %v", err)
}
if err := ioutil.WriteFile(cachedtoken, data, 0644); err != nil {
fmt.Errorf("(WARNING) Unable to write token to local cache.\n")
}
}
return token, config, nil
}