Skip to content

Instantly share code, notes, and snippets.

@guumaster
Last active July 19, 2020 15:36
Show Gist options
  • Save guumaster/2c7f48ac3567ae6c456f4020c857c375 to your computer and use it in GitHub Desktop.
Save guumaster/2c7f48ac3567ae6c456f4020c857c375 to your computer and use it in GitHub Desktop.
Golang CLI oauth2

Using Google oAuth2 on a CLI

This simple gist shows how to get an oAuth2 token and auto-refresh when needed.

Files

  • main.go - main file where you need to set your credentials and needed scopes.
  • token.go - token manager hides all the complexity of read/save/update the token.
  • google_apis.go - contains some example functions to access Google APIs like Drive, People and Spreadsheets.

How it works

The TokenManager object will handle the token for you by doing the following:

  • Reads the token from a file.
  • If the token is not present or not valid, starts an oAuth2 flow using nmrshll/oauth2-noserver to launch the oAuth flow (no copy&paste the token on your console!).
  • Creates a TokenSource and access the token. This will trigger a refresh call if the token is expired.
  • If the the token obtained is new, it will update the json file with the new data.

Why?

It took me some time to get this working (I love/hate oAuth), so here is a working example that may be useful for someone else.

package main
import (
"log"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/drive/v3"
"google.golang.org/api/people/v1"
)
const spreadsheetId = "__SOME_SPREAD_SHEET_ID__"
func main() {
config := &oauth2.Config{
ClientID: "__YOUR_GOOGLE_CLIENT_ID__",
ClientSecret: "__YOUR_GOOGLE_SECRET__",
Scopes: []string{
people.UserinfoEmailScope,
people.UserinfoProfileScope,
drive.DriveScope,
},
Endpoint: google.Endpoint,
}
m, err := New(config, "token.json")
if err != nil {
log.Fatalf("error getting token: %v", err)
}
showDriveFiles(m)
showUserInfo(m)
updateSpreadSheet(m)
}
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
oauth2ns "github.com/nmrshll/oauth2-noserver"
"golang.org/x/oauth2"
)
var (
TokenNotFoundErr = errors.New("token not found")
TokenWebErr = errors.New("error getting token from web")
TokenOpenErr = errors.New("error opening token file")
TokenSaveErr = errors.New("error saving token file")
)
type TokenManager struct {
conf *oauth2.Config
token *oauth2.Token
originalAccessToken string
filepath string
}
func New(conf *oauth2.Config, filepath string) (*TokenManager, error) {
t := &TokenManager{
conf: conf,
filepath: filepath,
}
isNewToken := false
tok, err := t.getFromFile()
if errors.Is(err, TokenOpenErr) || errors.Is(err, TokenNotFoundErr) {
tok, err = t.getFromWeb()
if err != nil {
return nil, fmt.Errorf("error getting token from web: %w", err)
}
isNewToken = true
}
if err != nil {
return nil, fmt.Errorf("error getting token: %w", err)
}
t.token = tok
t.originalAccessToken = tok.AccessToken
// This will refresh the token when needed
ts := t.TokenSource(context.Background())
newTok, err := ts.Token()
if err != nil {
return nil, err
}
tokenRefreshed := tok.AccessToken != newTok.AccessToken
if isNewToken || tokenRefreshed {
t.token = newTok
err = t.save()
if err != nil {
return nil, err
}
}
return t, nil
}
func (t *TokenManager) TokenSource(ctx context.Context) oauth2.TokenSource {
return t.conf.TokenSource(ctx, t.token)
}
// Save stores the token in json file.
func (t *TokenManager) save() error {
fmt.Printf("Saving token to file: %s\n", t.filepath)
f, err := os.OpenFile(t.filepath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("%q: %w", err, TokenSaveErr)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
err = enc.Encode(t.token)
if err != nil {
return fmt.Errorf("%q: %w", err, TokenSaveErr)
}
return nil
}
// getFromFile retrieves a token from a local file.
func (t *TokenManager) getFromFile() (*oauth2.Token, error) {
f, err := os.Open(t.filepath)
if err != nil {
return nil, fmt.Errorf("%q: %w", err, TokenOpenErr)
}
defer f.Close()
tok := new(oauth2.Token)
err = json.NewDecoder(f).Decode(tok)
if err != nil {
return nil, fmt.Errorf("%q: %w", err, TokenOpenErr)
}
t.originalAccessToken = tok.AccessToken
return tok, err
}
// getFromWeb Starts a local server and the oauth flow
func (t *TokenManager) getFromWeb() (*oauth2.Token, error) {
client, err := oauth2ns.AuthenticateUser(t.conf)
if err != nil {
return nil, fmt.Errorf("%q: %w", err, TokenWebErr)
}
return client.Token, nil
}
package main
import (
"context"
"fmt"
"log"
"time"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
"google.golang.org/api/people/v1"
"google.golang.org/api/sheets/v4"
)
func showDriveFiles(m *TokenManager) {
ctx := context.Background()
ts := m.TokenSource(ctx)
srv, err := drive.NewService(ctx, option.WithTokenSource(ts))
if err != nil {
log.Fatalf("Unable to retrieve Drive client: %v", err)
}
r, err := srv.Files.List().
PageSize(40).
Fields("nextPageToken, files(id, name)").
Do()
if err != nil {
log.Fatalf("Unable to retrieve files: %v", err)
}
fmt.Println("Files:")
if len(r.Files) == 0 {
fmt.Println("No files found.")
} else {
for _, i := range r.Files {
fmt.Printf("%s (%s)\n", i.Name, i.Id)
}
}
}
func showUserInfo(m *TokenManager) {
ctx := context.Background()
ts := m.TokenSource(ctx)
p, err := people.NewService(ctx, option.WithTokenSource(ts))
if err != nil {
log.Fatal(err)
}
tokenUser, err := p.People.
Get("people/me").
PersonFields("names,nicknames,emailAddresses").
Context(ctx).
Do()
if err != nil {
log.Fatal(err)
}
name := tokenUser.Names[0].DisplayName
email := tokenUser.EmailAddresses[0].Value
fmt.Printf("User: %s <%s>\n", name, email)
}
func updateSpreadSheet(m *TokenManager) {
ctx := context.Background()
ts := m.TokenSource(ctx)
sheet, err := sheets.NewService(ctx, option.WithTokenSource(ts))
if err != nil {
log.Fatalf("Unable to retrieve Sheets client: %v", err)
}
readRange := "Main!A1:E"
resp, err := sheet.Spreadsheets.Values.Get(spreadsheetId, readRange).Do()
if err != nil {
log.Fatalf("Unable to retrieve data from sheet: %v", err)
}
v := &sheets.ValueRange{
MajorDimension: "ROWS",
Range: resp.Range,
Values: [][]interface{}{
{"hello", "world!", time.Now().UTC().Format(time.RFC3339)},
},
}
_, err = sheet.Spreadsheets.Values.
Append(spreadsheetId, resp.Range, v).
ValueInputOption("USER_ENTERED").
Context(ctx).
Do()
if err != nil {
log.Fatal(err)
}
if len(resp.Values) == 0 {
fmt.Println("No data found.")
} else {
fmt.Println("Msg [date]:")
for _, row := range resp.Values {
// Print columns A to C
t, _ := time.Parse(time.RFC3339, row[2].(string))
fmt.Printf("%s %s [%s]\n", row[0], row[1], t.Format(time.RFC822Z))
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment