1package gcputil
2
3import (
4	"context"
5	"crypto/x509"
6	"encoding/base64"
7	"encoding/json"
8	"encoding/pem"
9	"errors"
10	"fmt"
11	"github.com/hashicorp/go-cleanhttp"
12	"github.com/mitchellh/go-homedir"
13	"golang.org/x/oauth2"
14	"golang.org/x/oauth2/google"
15	"golang.org/x/oauth2/jwt"
16	googleoauth2 "google.golang.org/api/oauth2/v2"
17	"gopkg.in/square/go-jose.v2"
18	"io/ioutil"
19	"net/http"
20	"os"
21	"path/filepath"
22	"strings"
23)
24
25const (
26	labelRegex                 string = "^(?P<key>[a-z]([\\w-]+)?):(?P<value>[\\w-]*)$"
27	defaultHomeCredentialsFile        = ".gcp/credentials"
28)
29
30// GcpCredentials represents a simplified version of the Google Cloud Platform credentials file format.
31type GcpCredentials struct {
32	ClientEmail  string `json:"client_email" structs:"client_email" mapstructure:"client_email"`
33	ClientId     string `json:"client_id" structs:"client_id" mapstructure:"client_id"`
34	PrivateKeyId string `json:"private_key_id" structs:"private_key_id" mapstructure:"private_key_id"`
35	PrivateKey   string `json:"private_key" structs:"private_key" mapstructure:"private_key"`
36	ProjectId    string `json:"project_id" structs:"project_id" mapstructure:"project_id"`
37}
38
39// FindCredentials attempts to obtain GCP credentials in the
40// following ways:
41// * Parse JSON from provided credentialsJson
42// * Parse JSON from the environment variables GOOGLE_CREDENTIALS or GOOGLE_CLOUD_KEYFILE_JSON
43// * Parse JSON file ~/.gcp/credentials
44// * Google Application Default Credentials (see https://developers.google.com/identity/protocols/application-default-credentials)
45func FindCredentials(credsJson string, ctx context.Context, scopes ...string) (*GcpCredentials, oauth2.TokenSource, error) {
46	var creds *GcpCredentials
47	var err error
48	// 1. Parse JSON from provided credentialsJson
49	if credsJson == "" {
50		// 2. JSON from env var GOOGLE_CREDENTIALS
51		credsJson = os.Getenv("GOOGLE_CREDENTIALS")
52	}
53
54	if credsJson == "" {
55		// 3. JSON from env var GOOGLE_CLOUD_KEYFILE_JSON
56		credsJson = os.Getenv("GOOGLE_CLOUD_KEYFILE_JSON")
57	}
58
59	if credsJson == "" {
60		// 4. JSON from ~/.gcp/credentials
61		home, err := homedir.Dir()
62		if err != nil {
63			return nil, nil, errors.New("could not find home directory")
64		}
65		credBytes, err := ioutil.ReadFile(filepath.Join(home, defaultHomeCredentialsFile))
66		if err == nil {
67			credsJson = string(credBytes)
68		}
69	}
70
71	// Parse JSON into credentials.
72	if credsJson != "" {
73		creds, err = Credentials(credsJson)
74		if err == nil {
75			conf := jwt.Config{
76				Email:      creds.ClientEmail,
77				PrivateKey: []byte(creds.PrivateKey),
78				Scopes:     scopes,
79				TokenURL:   "https://accounts.google.com/o/oauth2/token",
80			}
81			return creds, conf.TokenSource(ctx), nil
82		}
83	}
84
85	// 5. Use Application default credentials.
86	defaultCreds, err := google.FindDefaultCredentials(ctx, scopes...)
87	if err != nil {
88		return nil, nil, err
89	}
90
91	if defaultCreds.JSON != nil {
92		creds, err = Credentials(string(defaultCreds.JSON))
93		if err != nil {
94			return nil, nil, errors.New("could not read credentials from application default credential JSON")
95		}
96	}
97
98	return creds, defaultCreds.TokenSource, nil
99}
100
101// Credentials attempts to parse GcpCredentials from a JSON string.
102func Credentials(credentialsJson string) (*GcpCredentials, error) {
103	credentials := &GcpCredentials{}
104	if err := json.Unmarshal([]byte(credentialsJson), &credentials); err != nil {
105		return nil, err
106	}
107	return credentials, nil
108}
109
110// GetHttpClient creates an HTTP client from the given Google credentials and scopes.
111func GetHttpClient(credentials *GcpCredentials, clientScopes ...string) (*http.Client, error) {
112	conf := jwt.Config{
113		Email:      credentials.ClientEmail,
114		PrivateKey: []byte(credentials.PrivateKey),
115		Scopes:     clientScopes,
116		TokenURL:   "https://accounts.google.com/o/oauth2/token",
117	}
118
119	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cleanhttp.DefaultClient())
120	client := conf.Client(ctx)
121	return client, nil
122}
123
124// PublicKey returns a public key from a Google PEM key file (type TYPE_X509_PEM_FILE).
125func PublicKey(pemString string) (interface{}, error) {
126	pemBytes, err := base64.StdEncoding.DecodeString(pemString)
127	if err != nil {
128		return nil, err
129	}
130
131	block, _ := pem.Decode(pemBytes)
132	if block == nil {
133		return nil, errors.New("Unable to find pem block in key")
134	}
135
136	cert, err := x509.ParseCertificate(block.Bytes)
137	if err != nil {
138		return nil, err
139	}
140
141	return cert.PublicKey, nil
142}
143
144// OAuth2RSAPublicKey returns the PEM key file string for Google Oauth2 public cert for the given 'kid' id.
145func OAuth2RSAPublicKey(kid, oauth2BasePath string) (interface{}, error) {
146	oauth2Client, err := googleoauth2.New(cleanhttp.DefaultClient())
147	if err != nil {
148		return "", err
149	}
150
151	if len(oauth2BasePath) > 0 {
152		oauth2Client.BasePath = oauth2BasePath
153	}
154
155	jwks, err := oauth2Client.GetCertForOpenIdConnect().Do()
156	if err != nil {
157		return nil, err
158	}
159
160	for _, key := range jwks.Keys {
161		if key.Kid == kid && jose.SignatureAlgorithm(key.Alg) == jose.RS256 {
162			// Trim extra '=' from key so it can be parsed.
163			key.N = strings.TrimRight(key.N, "=")
164			js, err := key.MarshalJSON()
165			if err != nil {
166				return nil, fmt.Errorf("unable to marshal json %v", err)
167			}
168			key := &jose.JSONWebKey{}
169			if err := key.UnmarshalJSON(js); err != nil {
170				return nil, fmt.Errorf("unable to unmarshal json %v", err)
171			}
172
173			return key.Key, nil
174		}
175	}
176
177	return nil, fmt.Errorf("could not find public key with kid '%s'", kid)
178}
179