From 3c060f4d33530866496515b45fcfaa3fd2da5f32 Mon Sep 17 00:00:00 2001
From: Rohan Fletcher <rohan@ii.coop>
Date: Thu, 31 Jan 2019 20:31:42 +1300
Subject: [PATCH] Added support for external Google API OAuth2 client
 credentials

---
 auth/file_source.go | 10 ++++++++++
 auth/oauth.go       | 11 ++++-------
 handlers_drive.go   | 34 +++++++++++++++++++++++++++++-----
 3 files changed, 43 insertions(+), 12 deletions(-)

diff --git a/auth/file_source.go b/auth/file_source.go
index 9223d1dc..9c6eff9a 100644
--- a/auth/file_source.go
+++ b/auth/file_source.go
@@ -3,6 +3,7 @@ package auth
 import (
 	"encoding/json"
 	"golang.org/x/oauth2"
+	"golang.org/x/oauth2/google"
 	"io/ioutil"
 	"os"
 )
@@ -43,6 +44,15 @@ func ReadFile(path string) ([]byte, bool, error) {
 	return content, true, nil
 }
 
+func ReadClientCredentials(path string) (*oauth2.Config, bool, error) {
+
+	content, exists, err := ReadFile(path)
+	if err != nil || exists == false {
+		return nil, exists, err
+	}
+	conf, err := google.ConfigFromJSON(content)
+	return conf, exists, err
+}
 
 func ReadToken(path string) (*oauth2.Token, bool, error) {
 
diff --git a/auth/oauth.go b/auth/oauth.go
index bc567385..5e09e08b 100644
--- a/auth/oauth.go
+++ b/auth/oauth.go
@@ -10,8 +10,7 @@ import (
 
 type authCodeFn func(string) func() string
 
-func NewFileSourceClient(clientId, clientSecret, tokenFile string, authFn authCodeFn) (*http.Client, error) {
-	conf := getConfig(clientId, clientSecret)
+func NewFileSourceClient(conf *oauth2.Config, tokenFile string, authFn authCodeFn) (*http.Client, error) {
 
 	// Read cached token
 	token, exists, err := ReadToken(tokenFile)
@@ -36,8 +35,7 @@ func NewFileSourceClient(clientId, clientSecret, tokenFile string, authFn authCo
 	), nil
 }
 
-func NewRefreshTokenClient(clientId, clientSecret, refreshToken string) *http.Client {
-	conf := getConfig(clientId, clientSecret)
+func NewRefreshTokenClient(conf *oauth2.Config, refreshToken string) *http.Client {
 
 	token := &oauth2.Token{
 		TokenType:    "Bearer",
@@ -51,8 +49,7 @@ func NewRefreshTokenClient(clientId, clientSecret, refreshToken string) *http.Cl
 	)
 }
 
-func NewAccessTokenClient(clientId, clientSecret, accessToken string) *http.Client {
-	conf := getConfig(clientId, clientSecret)
+func NewAccessTokenClient(conf *oauth2.Config, accessToken string) *http.Client {
 
 	token := &oauth2.Token{
 		TokenType:   "Bearer",
@@ -82,7 +79,7 @@ func NewServiceAccountClient(serviceAccountFile string) (*http.Client, error) {
 	return conf.Client(oauth2.NoContext), nil
 }
 
-func getConfig(clientId, clientSecret string) *oauth2.Config {
+func AssembleClientCredentials(clientId, clientSecret string) *oauth2.Config {
 	return &oauth2.Config{
 		ClientID:     clientId,
 		ClientSecret: clientSecret,
diff --git a/handlers_drive.go b/handlers_drive.go
index 7bda872f..c3f52a49 100644
--- a/handlers_drive.go
+++ b/handlers_drive.go
@@ -17,8 +17,11 @@ import (
 const ClientId = "367116221053-7n0vf5akeru7on6o2fjinrecpdoe99eg.apps.googleusercontent.com"
 const ClientSecret = "1qsNodXNaWq1mQuBjUjmvhoO"
 const TokenFilename = "token_v2.json"
+const ClientCredentialsFilename = "client_id.json"
 const DefaultCacheFileName = "file_cache.json"
 
+var usingClientCredentialsFile = false
+
 func listHandler(ctx cli.Context) {
 	args := ctx.Args()
 	err := newDrive(args).List(drive.ListFilesArgs{
@@ -345,16 +348,31 @@ func getOauthClient(args cli.Arguments) (*http.Client, error) {
 		ExitF("Access token not needed when refresh token is provided")
 	}
 
+	configDir := getConfigDir(args)
+
+	clientCredentialsPath := ConfigFilePath(configDir, ClientCredentialsFilename)
+	clientCredentials, exists, err := auth.ReadClientCredentials(clientCredentialsPath)
+	if err != nil {
+		ExitF("Failed to read client credentials file: %s", err)
+	} else if !exists {
+		clientCredentials = auth.AssembleClientCredentials(ClientId, ClientSecret)
+	} else {
+		usingClientCredentialsFile = true
+		// Make sure the google drive scope is present
+		if len(clientCredentials.Scopes) == 0 {
+			clientCredentials.Scopes = append(clientCredentials.Scopes, "https://www.googleapis.com/auth/drive")
+		}
+	}
+
+
 	if args.String("refreshToken") != "" {
-		return auth.NewRefreshTokenClient(ClientId, ClientSecret, args.String("refreshToken")), nil
+		return auth.NewRefreshTokenClient(clientCredentials, args.String("refreshToken")), nil
 	}
 
 	if args.String("accessToken") != "" {
-		return auth.NewAccessTokenClient(ClientId, ClientSecret, args.String("accessToken")), nil
+		return auth.NewAccessTokenClient(clientCredentials, args.String("accessToken")), nil
 	}
 
-	configDir := getConfigDir(args)
-
 	if args.String("serviceAccount") != "" {
 		serviceAccountPath := ConfigFilePath(configDir, args.String("serviceAccount"))
 		serviceAccountClient, err := auth.NewServiceAccountClient(serviceAccountPath)
@@ -365,7 +383,7 @@ func getOauthClient(args cli.Arguments) (*http.Client, error) {
 	}
 
 	tokenPath := ConfigFilePath(configDir, TokenFilename)
-	return auth.NewFileSourceClient(ClientId, ClientSecret, tokenPath, authCodePrompt)
+	return auth.NewFileSourceClient(clientCredentials, tokenPath, authCodePrompt)
 }
 
 func getConfigDir(args cli.Arguments) string {
@@ -392,6 +410,12 @@ func newDrive(args cli.Arguments) *drive.Drive {
 
 func authCodePrompt(url string) func() string {
 	return func() string {
+		if (usingClientCredentialsFile == true) {
+			fmt.Println("Client credentials loaded from file\n")
+		} else {
+			fmt.Println("Client credentials file not found. Using built-in defaults\n")
+		}
+
 		fmt.Println("Authentication needed")
 		fmt.Println("Go to the following url in your browser:")
 		fmt.Printf("%s\n\n", url)
