blob: e87d89d8675cc38ee4251f32377a544f0535782d [file] [log] [blame]
//
// 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
}