1// Copyright 2014 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package google
6
7import (
8	"context"
9	"encoding/json"
10	"errors"
11	"fmt"
12	"net/url"
13	"strings"
14	"time"
15
16	"cloud.google.com/go/compute/metadata"
17	"golang.org/x/oauth2"
18	"golang.org/x/oauth2/jwt"
19)
20
21// Endpoint is Google's OAuth 2.0 endpoint.
22var Endpoint = oauth2.Endpoint{
23	AuthURL:   "https://accounts.google.com/o/oauth2/auth",
24	TokenURL:  "https://oauth2.googleapis.com/token",
25	AuthStyle: oauth2.AuthStyleInParams,
26}
27
28// JWTTokenURL is Google's OAuth 2.0 token URL to use with the JWT flow.
29const JWTTokenURL = "https://oauth2.googleapis.com/token"
30
31// ConfigFromJSON uses a Google Developers Console client_credentials.json
32// file to construct a config.
33// client_credentials.json can be downloaded from
34// https://console.developers.google.com, under "Credentials". Download the Web
35// application credentials in the JSON format and provide the contents of the
36// file as jsonKey.
37func ConfigFromJSON(jsonKey []byte, scope ...string) (*oauth2.Config, error) {
38	type cred struct {
39		ClientID     string   `json:"client_id"`
40		ClientSecret string   `json:"client_secret"`
41		RedirectURIs []string `json:"redirect_uris"`
42		AuthURI      string   `json:"auth_uri"`
43		TokenURI     string   `json:"token_uri"`
44	}
45	var j struct {
46		Web       *cred `json:"web"`
47		Installed *cred `json:"installed"`
48	}
49	if err := json.Unmarshal(jsonKey, &j); err != nil {
50		return nil, err
51	}
52	var c *cred
53	switch {
54	case j.Web != nil:
55		c = j.Web
56	case j.Installed != nil:
57		c = j.Installed
58	default:
59		return nil, fmt.Errorf("oauth2/google: no credentials found")
60	}
61	if len(c.RedirectURIs) < 1 {
62		return nil, errors.New("oauth2/google: missing redirect URL in the client_credentials.json")
63	}
64	return &oauth2.Config{
65		ClientID:     c.ClientID,
66		ClientSecret: c.ClientSecret,
67		RedirectURL:  c.RedirectURIs[0],
68		Scopes:       scope,
69		Endpoint: oauth2.Endpoint{
70			AuthURL:  c.AuthURI,
71			TokenURL: c.TokenURI,
72		},
73	}, nil
74}
75
76// JWTConfigFromJSON uses a Google Developers service account JSON key file to read
77// the credentials that authorize and authenticate the requests.
78// Create a service account on "Credentials" for your project at
79// https://console.developers.google.com to download a JSON key file.
80func JWTConfigFromJSON(jsonKey []byte, scope ...string) (*jwt.Config, error) {
81	var f credentialsFile
82	if err := json.Unmarshal(jsonKey, &f); err != nil {
83		return nil, err
84	}
85	if f.Type != serviceAccountKey {
86		return nil, fmt.Errorf("google: read JWT from JSON credentials: 'type' field is %q (expected %q)", f.Type, serviceAccountKey)
87	}
88	scope = append([]string(nil), scope...) // copy
89	return f.jwtConfig(scope), nil
90}
91
92// JSON key file types.
93const (
94	serviceAccountKey  = "service_account"
95	userCredentialsKey = "authorized_user"
96)
97
98// credentialsFile is the unmarshalled representation of a credentials file.
99type credentialsFile struct {
100	Type string `json:"type"` // serviceAccountKey or userCredentialsKey
101
102	// Service Account fields
103	ClientEmail  string `json:"client_email"`
104	PrivateKeyID string `json:"private_key_id"`
105	PrivateKey   string `json:"private_key"`
106	TokenURL     string `json:"token_uri"`
107	ProjectID    string `json:"project_id"`
108
109	// User Credential fields
110	// (These typically come from gcloud auth.)
111	ClientSecret string `json:"client_secret"`
112	ClientID     string `json:"client_id"`
113	RefreshToken string `json:"refresh_token"`
114}
115
116func (f *credentialsFile) jwtConfig(scopes []string) *jwt.Config {
117	cfg := &jwt.Config{
118		Email:        f.ClientEmail,
119		PrivateKey:   []byte(f.PrivateKey),
120		PrivateKeyID: f.PrivateKeyID,
121		Scopes:       scopes,
122		TokenURL:     f.TokenURL,
123	}
124	if cfg.TokenURL == "" {
125		cfg.TokenURL = JWTTokenURL
126	}
127	return cfg
128}
129
130func (f *credentialsFile) tokenSource(ctx context.Context, scopes []string) (oauth2.TokenSource, error) {
131	switch f.Type {
132	case serviceAccountKey:
133		cfg := f.jwtConfig(scopes)
134		return cfg.TokenSource(ctx), nil
135	case userCredentialsKey:
136		cfg := &oauth2.Config{
137			ClientID:     f.ClientID,
138			ClientSecret: f.ClientSecret,
139			Scopes:       scopes,
140			Endpoint:     Endpoint,
141		}
142		tok := &oauth2.Token{RefreshToken: f.RefreshToken}
143		return cfg.TokenSource(ctx, tok), nil
144	case "":
145		return nil, errors.New("missing 'type' field in credentials")
146	default:
147		return nil, fmt.Errorf("unknown credential type: %q", f.Type)
148	}
149}
150
151// ComputeTokenSource returns a token source that fetches access tokens
152// from Google Compute Engine (GCE)'s metadata server. It's only valid to use
153// this token source if your program is running on a GCE instance.
154// If no account is specified, "default" is used.
155// If no scopes are specified, a set of default scopes are automatically granted.
156// Further information about retrieving access tokens from the GCE metadata
157// server can be found at https://cloud.google.com/compute/docs/authentication.
158func ComputeTokenSource(account string, scope ...string) oauth2.TokenSource {
159	return oauth2.ReuseTokenSource(nil, computeSource{account: account, scopes: scope})
160}
161
162type computeSource struct {
163	account string
164	scopes  []string
165}
166
167func (cs computeSource) Token() (*oauth2.Token, error) {
168	if !metadata.OnGCE() {
169		return nil, errors.New("oauth2/google: can't get a token from the metadata service; not running on GCE")
170	}
171	acct := cs.account
172	if acct == "" {
173		acct = "default"
174	}
175	tokenURI := "instance/service-accounts/" + acct + "/token"
176	if len(cs.scopes) > 0 {
177		v := url.Values{}
178		v.Set("scopes", strings.Join(cs.scopes, ","))
179		tokenURI = tokenURI + "?" + v.Encode()
180	}
181	tokenJSON, err := metadata.Get(tokenURI)
182	if err != nil {
183		return nil, err
184	}
185	var res struct {
186		AccessToken  string `json:"access_token"`
187		ExpiresInSec int    `json:"expires_in"`
188		TokenType    string `json:"token_type"`
189	}
190	err = json.NewDecoder(strings.NewReader(tokenJSON)).Decode(&res)
191	if err != nil {
192		return nil, fmt.Errorf("oauth2/google: invalid token JSON from metadata: %v", err)
193	}
194	if res.ExpiresInSec == 0 || res.AccessToken == "" {
195		return nil, fmt.Errorf("oauth2/google: incomplete token received from metadata")
196	}
197	tok := &oauth2.Token{
198		AccessToken: res.AccessToken,
199		TokenType:   res.TokenType,
200		Expiry:      time.Now().Add(time.Duration(res.ExpiresInSec) * time.Second),
201	}
202	// NOTE(cbro): add hidden metadata about where the token is from.
203	// This is needed for detection by client libraries to know that credentials come from the metadata server.
204	// This may be removed in a future version of this library.
205	return tok.WithExtra(map[string]interface{}{
206		"oauth2.google.tokenSource":    "compute-metadata",
207		"oauth2.google.serviceAccount": acct,
208	}), nil
209}
210