Skip to content

Instantly share code, notes, and snippets.

@bbengfort

bbengfort/auth.go

Created Apr 20, 2017
Embed
What would you like to do?
OAuth2 token cacheing and management
package daytimer
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
calendar "google.golang.org/api/calendar/v3"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
)
// Authentication is a wrapper for the OAuth token authentication process,
// reading and writing the token from a credentials file on disk. It is meant
// to perform 100% of the workflow of authentication.
type Authentication struct {
cachePath string // the path to the token cache file on disk
configPath string // the path to the client_secret.json file on disk
token *oauth2.Token // the oauth token cached in the cache file
}
// Token returns the authentication token by first looking on disk for the
// cache, and if it doesn't exist by executing the authentication.
func (auth *Authentication) Token() (*oauth2.Token, error) {
// Load the token from the cache if it doesn't exist.
if auth.token == nil {
if err := auth.Load(""); err != nil {
// If we cannot load the token from disk, authenticate
if err != nil {
if err = auth.Authenticate(); err != nil {
return nil, err
}
}
}
}
// Return the token
return auth.token, nil
}
// Authenticate runs an interactive authentication on the command line,
// prompting the user to open a brower page and enter an authorization code.
// It will then fetch an token via OAuth and cache it as credentials. Note
// that this method will overwrite any previously cached token.
func (auth *Authentication) Authenticate() error {
// Load and create the OAuth2 Configuration
config, err := auth.Config()
if err != nil {
return err
}
// Compute the URL for the authoerization
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
// Notify the user of the web browser.
fmt.Println("In order to authenticate, use a browser to authorize daytimer with Google")
// Open the web browser
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", authURL).Start()
case "windows", "darwin":
err = exec.Command("open", authURL).Start()
default:
err = fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
// If we couldn't open the web browser, prompt the user to do it manually.
if err != nil {
fmt.Printf("Copy and paste the following link: \n%s\n\n", authURL)
}
// Prompt for the authorization code
code, err := Prompt("enter authorization code:")
if err != nil {
return fmt.Errorf("unable to read authorization code %v", err)
}
// Perform the exchange for the token
token, err := config.Exchange(oauth2.NoContext, code)
if err != nil {
return fmt.Errorf("unable to retrieve token from web %v", err)
}
// Cache the token to disk
auth.token = token
auth.Save("")
return nil
}
// Config loads the client_secret.json from the ConfigPath. It is used both to
// create the client for requests as well as to perform authentication.
func (auth *Authentication) Config() (*oauth2.Config, error) {
path, err := auth.ConfigPath()
if err != nil {
return nil, err
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("unable to read client secret file: %v", err)
}
config, err := google.ConfigFromJSON(data, calendar.CalendarReadonlyScope)
if err != nil {
return nil, fmt.Errorf("unable to parse client secret file to config: %v", err)
}
return config, nil
}
// CachePath computes the path to the credential token file, creating the
// directory if necessary and stores it in the authentication struct.
func (auth *Authentication) CachePath() (string, error) {
if auth.cachePath == "" {
// Get the user to look up the user's home directory
usr, err := user.Current()
if err != nil {
return "", err
}
// Get the hidden credentials directory, making sure it's created
cacheDir := filepath.Join(usr.HomeDir, ".daytimer")
os.MkdirAll(cacheDir, 0700)
// Determine the path to the token cache file
cacheFile := url.QueryEscape("credentials.json")
auth.cachePath = filepath.Join(cacheDir, cacheFile)
}
return auth.cachePath, nil
}
// ConfigPath computes the path to the configuration file, client_secret.json.
func (auth *Authentication) ConfigPath() (string, error) {
if auth.configPath == "" {
// Get the user to look up the user's home directory
usr, err := user.Current()
if err != nil {
return "", err
}
// Create the path to the default configuration
auth.configPath = filepath.Join(
usr.HomeDir, ".daytimer", "client_secret.json",
)
}
return auth.configPath, nil
}
// Load the token from the specified path (and saves the path to the struct).
// If path is an empty string then it will load the token from the default
// cache path in the home directory. This method returns an error if the token
// cannot be loaded from the file.
func (auth *Authentication) Load(path string) error {
var err error
// Get the default cache path or save the specified path
if path == "" {
path, err = auth.CachePath()
if err != nil {
return err
}
} else {
auth.cachePath = path
}
// Open the file at the path
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("could not open cache file at %s: %v", path, err)
}
defer f.Close()
// Decode the JSON token cache
auth.token = new(oauth2.Token)
if err := json.NewDecoder(f).Decode(auth.token); err != nil {
return fmt.Errorf("could not decode token in cache file at %s: %v", path, err)
}
return nil
}
// Save the token to the specified path (and save the path to the struct).
// If the path is empty, then it will save the path to the current CachePath.
func (auth *Authentication) Save(path string) error {
var err error
// Get the default cache path or save the specified path
if path == "" {
path, err = auth.CachePath()
if err != nil {
return err
}
} else {
auth.cachePath = path
}
// Open the file for writing
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("unable to cache oauth token: %v", err)
}
defer f.Close()
// Encode the token and write to disk
if err := json.NewEncoder(f).Encode(auth.token); err != nil {
return fmt.Errorf("could not encode oauth token: %v", err)
}
return nil
}
// Delete the token file at the given path in order to force a
// reauthentication. This method also saves the given path, or if the path is
// empty, then it will compute the default CachePath. This method will not
// return an error on failure (e.g. if the file does not exist).
func (auth *Authentication) Delete(path string) {
// Get the default cache path or save the specified path
if path == "" {
path, _ = auth.CachePath()
} else {
auth.cachePath = path
}
// Delete the file at the cache path if it exists
os.Remove(path)
}
@jayhuang75

This comment has been minimized.

Copy link

@jayhuang75 jayhuang75 commented Aug 22, 2017

Hello @bbengfort, Thank you so much for this snippet code, and I try my self and get some error special for the 81 line
code, err := Prompt("enter authorization code:")
Please advise what package you are using? for the Prompt

Thanks

@alok87

This comment has been minimized.

Copy link

@alok87 alok87 commented Oct 26, 2017

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.