| // |
| // Copyright 2017 Google Inc. All Rights Reserved. |
| // |
| // 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 googleauth |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "net" |
| "net/http" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| |
| "golang.org/x/net/context" |
| "golang.org/x/oauth2" |
| ) |
| |
| 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 |
| ` |
| |
| var ( |
| clientSecretsFile = flag.String("secrets", "client_secrets.json", "Client Secrets configuration") |
| cacheFile = flag.String("cache", "request.token", "Token cache file") |
| ) |
| |
| // ClientConfig is a data structure definition for the client_secrets.json file. |
| // The code unmarshals the JSON configuration file into this structure. |
| type ClientConfig struct { |
| ClientID string `json:"client_id"` |
| ClientSecret string `json:"client_secret"` |
| RedirectURIs []string `json:"redirect_uris"` |
| AuthURI string `json:"auth_uri"` |
| TokenURI string `json:"token_uri"` |
| } |
| |
| // Config is a root-level configuration object. |
| type Config struct { |
| Installed ClientConfig `json:"installed"` |
| Web ClientConfig `json:"web"` |
| } |
| |
| // 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 err error |
| switch runtime.GOOS { |
| case "linux": |
| err = exec.Command("xdg-open", url).Start() |
| case "windows": |
| err = exec.Command("rundll32", "url.dll,FileProtocolHandler", "http://localhost:4001/").Start() |
| case "darwin": |
| err = exec.Command("open", url).Start() |
| default: |
| err = fmt.Errorf("Cannot open URL %s on this platform", url) |
| } |
| return err |
| } |
| |
| // readConfig reads the configuration from clientSecretsFile. |
| // It returns an oauth configuration object for use with the Google API client. |
| func readConfig(scopes []string) (*oauth2.Config, error) { |
| // Read the secrets file |
| data, err := ioutil.ReadFile(*clientSecretsFile) |
| if err != nil { |
| pwd, _ := os.Getwd() |
| fullPath := filepath.Join(pwd, *clientSecretsFile) |
| return nil, fmt.Errorf(missingClientSecretsMessage, fullPath) |
| } |
| |
| cfg := new(Config) |
| err = json.Unmarshal(data, &cfg) |
| if err != nil { |
| return nil, err |
| } |
| |
| var redirectURI string |
| if len(cfg.Web.RedirectURIs) > 0 { |
| redirectURI = cfg.Web.RedirectURIs[0] |
| } else if len(cfg.Installed.RedirectURIs) > 0 { |
| redirectURI = cfg.Installed.RedirectURIs[0] |
| } else { |
| return nil, errors.New("Must specify a redirect URI in config file or when creating OAuth client") |
| } |
| |
| return &oauth2.Config{ |
| ClientID: cfg.Installed.ClientID, |
| ClientSecret: cfg.Installed.ClientSecret, |
| Scopes: scopes, |
| Endpoint: oauth2.Endpoint{cfg.Installed.AuthURI, cfg.Installed.TokenURI}, |
| RedirectURL: redirectURI, |
| }, 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 startWebServer() (codeCh chan string, err error) { |
| listener, err := net.Listen("tcp", "localhost:8080") |
| if err != nil { |
| 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 |
| } |
| |
| // NewOAuth2Client takes the user through the three-legged OAuth flow. |
| // It opens a browser in the native OS or outputs a URL, then blocks until |
| // the redirect completes to the /oauth2callback URI. |
| // It returns an instance of an HTTP client that can be passed to the |
| // constructor of an OAuth client. |
| // scopes is a variable number of OAuth scopes |
| func NewOAuth2Client(scopes ...string) (*http.Client, error) { |
| var ctx context.Context |
| tokenSource, err := NewOAuth2TokenSource(scopes...) |
| if err == nil { |
| return oauth2.NewClient(ctx, tokenSource), nil |
| } |
| return nil, err |
| } |
| |
| // NewOAuth2TokenSource takes the user through the three-legged OAuth flow. |
| // It opens a browser in the native OS or outputs a URL, then blocks until |
| // the redirect completes to the /oauth2callback URI. |
| // It returns an instance of an OAuth token source that can be passed to the |
| // constructor of an OAuth client. |
| // scopes is a variable number of OAuth scopes |
| func NewOAuth2TokenSource(scopes ...string) (oauth2.TokenSource, error) { |
| config, err := readConfig(scopes) |
| if err != nil { |
| msg := fmt.Sprintf("Cannot read configuration file: %v", err) |
| return nil, errors.New(msg) |
| } |
| |
| var ctx context.Context |
| |
| // 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. |
| //token, err := config.TokenCache.Token() |
| |
| var token *oauth2.Token |
| |
| data, err := ioutil.ReadFile(*cacheFile) |
| if err == nil { |
| err = json.Unmarshal(data, &token) |
| } |
| if (err != nil) || !token.Valid() { |
| // Start web server. |
| // This is how this program receives the authorization code |
| // when the browser redirects. |
| codeCh, err := startWebServer() |
| if err != nil { |
| return nil, err |
| } |
| |
| // Open url in browser |
| url := config.AuthCodeURL("") |
| err = openURL(url) |
| if err != nil { |
| fmt.Println("Visit the URL below to get a code.", |
| " This program will pause until the site is visted.") |
| } else { |
| fmt.Println("Your browser has been opened to an authorization URL.", |
| " This program will resume once authorization has been provided.\n") |
| } |
| fmt.Println(url) |
| |
| // Wait for the web server to get the code. |
| code := <-codeCh |
| |
| // This code caches the authorization code on the local |
| // filesystem, if necessary, as long as the TokenCache |
| // attribute in the config is set. |
| token, err = config.Exchange(ctx, code) |
| if err != nil { |
| return nil, err |
| } |
| |
| data, err := json.Marshal(token) |
| ioutil.WriteFile(*cacheFile, data, 0644) |
| } |
| return oauth2.StaticTokenSource(token), nil |
| } |