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