google: refactor JWT parsing code internally

The ADC code and the JWT-parsing function operate on the same data
format, but were using separate code paths, each of which was missing
things from the other.

While this presents no change in API surface, JWTConfigFromJSON now
strictly checks the "type" field in the JSON file before building a
config.

Change-Id: I2f593a16bf4591059fbf9002bccea06e41e5e161
Reviewed-on: https://go-review.googlesource.com/32678
Reviewed-by: Jaana Burcu Dogan <jbd@google.com>
diff --git a/google/default.go b/google/default.go
index 565d731..c572d1a 100644
--- a/google/default.go
+++ b/google/default.go
@@ -6,7 +6,6 @@
 
 import (
 	"encoding/json"
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -17,7 +16,6 @@
 	"cloud.google.com/go/compute/metadata"
 	"golang.org/x/net/context"
 	"golang.org/x/oauth2"
-	"golang.org/x/oauth2/jwt"
 )
 
 // DefaultClient returns an HTTP Client that uses the
@@ -112,44 +110,9 @@
 	if err != nil {
 		return nil, err
 	}
-	var d struct {
-		// Common fields
-		Type     string
-		ClientID string `json:"client_id"`
-
-		// User Credential fields
-		ClientSecret string `json:"client_secret"`
-		RefreshToken string `json:"refresh_token"`
-
-		// Service Account fields
-		ClientEmail  string `json:"client_email"`
-		PrivateKeyID string `json:"private_key_id"`
-		PrivateKey   string `json:"private_key"`
-	}
-	if err := json.Unmarshal(b, &d); err != nil {
+	var f credentialsFile
+	if err := json.Unmarshal(b, &f); err != nil {
 		return nil, err
 	}
-	switch d.Type {
-	case "authorized_user":
-		cfg := &oauth2.Config{
-			ClientID:     d.ClientID,
-			ClientSecret: d.ClientSecret,
-			Scopes:       append([]string{}, scopes...), // copy
-			Endpoint:     Endpoint,
-		}
-		tok := &oauth2.Token{RefreshToken: d.RefreshToken}
-		return cfg.TokenSource(ctx, tok), nil
-	case "service_account":
-		cfg := &jwt.Config{
-			Email:      d.ClientEmail,
-			PrivateKey: []byte(d.PrivateKey),
-			Scopes:     append([]string{}, scopes...), // copy
-			TokenURL:   JWTTokenURL,
-		}
-		return cfg.TokenSource(ctx), nil
-	case "":
-		return nil, errors.New("missing 'type' field in credentials")
-	default:
-		return nil, fmt.Errorf("unknown credential type: %q", d.Type)
-	}
+	return f.tokenSource(ctx, scopes)
 }
diff --git a/google/google.go b/google/google.go
index a48d5bf..0704732 100644
--- a/google/google.go
+++ b/google/google.go
@@ -22,6 +22,7 @@
 	"time"
 
 	"cloud.google.com/go/compute/metadata"
+	"golang.org/x/net/context"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/jwt"
 )
@@ -85,26 +86,73 @@
 // Create a service account on "Credentials" for your project at
 // https://console.developers.google.com to download a JSON key file.
 func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
-	var key struct {
-		Email        string `json:"client_email"`
-		PrivateKey   string `json:"private_key"`
-		PrivateKeyID string `json:"private_key_id"`
-		TokenURL     string `json:"token_uri"`
-	}
-	if err := json.Unmarshal(jsonKey, &key); err != nil {
+	var f credentialsFile
+	if err := json.Unmarshal(jsonKey, &f); err != nil {
 		return nil, err
 	}
-	config := &jwt.Config{
-		Email:        key.Email,
-		PrivateKey:   []byte(key.PrivateKey),
-		PrivateKeyID: key.PrivateKeyID,
-		Scopes:       scope,
-		TokenURL:     key.TokenURL,
+	if f.Type != serviceAccountKey {
+		return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
 	}
-	if config.TokenURL == "" {
-		config.TokenURL = JWTTokenURL
+	scope = append([]string(nil), scope...) // copy
+	return f.jwtConfig(scope), nil
+}
+
+// JSON key file types.
+const (
+	serviceAccountKey  = "service_account"
+	userCredentialsKey = "authorized_user"
+)
+
+// credentialsFile is the unmarshalled representation of a credentials file.
+type credentialsFile struct {
+	Type string `json:"type"` // serviceAccountKey or userCredentialsKey
+
+	// Service Account fields
+	ClientEmail  string `json:"client_email"`
+	PrivateKeyID string `json:"private_key_id"`
+	PrivateKey   string `json:"private_key"`
+	TokenURL     string `json:"token_uri"`
+
+	// User Credential fields
+	// (These typically come from gcloud auth.)
+	ClientSecret string `json:"client_secret"`
+	ClientID     string `json:"client_id"`
+	RefreshToken string `json:"refresh_token"`
+}
+
+func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config {
+	cfg := &jwt.Config{
+		Email:        f.ClientEmail,
+		PrivateKey:   []byte(f.PrivateKey),
+		PrivateKeyID: f.PrivateKeyID,
+		Scopes:       scopes,
+		TokenURL:     f.TokenURL,
 	}
-	return config, nil
+	if cfg.TokenURL == "" {
+		cfg.TokenURL = JWTTokenURL
+	}
+	return cfg
+}
+
+func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) {
+	switch f.Type {
+	case serviceAccountKey:
+		cfg := f.jwtConfig(scopes)
+		return cfg.TokenSource(ctx), nil
+	case userCredentialsKey:
+		cfg := &oauth2.Config{
+			ClientID:     f.ClientID,
+			ClientSecret: f.ClientSecret,
+			Scopes:       scopes,
+			Endpoint:     Endpoint,
+		}
+		tok := &oauth2.Token{RefreshToken: f.RefreshToken}
+		return cfg.TokenSource(ctx, tok), nil
+	case "":
+		return nil, errors.New("missing 'type' field in credentials")
+	default:
+		return nil, fmt.Errorf("unknown credential type: %q", f.Type)
+	}
 }
 
 // ComputeTokenSource returns a token source that fetches access tokens