1// +build go1.13
2
3/*
4 *
5 * Copyright 2020 gRPC authors.
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *     http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 */
20
21// Package sts implements call credentials using STS (Security Token Service) as
22// defined in https://tools.ietf.org/html/rfc8693.
23//
24// Experimental
25//
26// Notice: All APIs in this package are experimental and may be changed or
27// removed in a later release.
28package sts
29
30import (
31	"bytes"
32	"context"
33	"crypto/tls"
34	"crypto/x509"
35	"encoding/json"
36	"errors"
37	"fmt"
38	"io/ioutil"
39	"net/http"
40	"net/url"
41	"sync"
42	"time"
43
44	"google.golang.org/grpc/credentials"
45	"google.golang.org/grpc/grpclog"
46)
47
48const (
49	// HTTP request timeout set on the http.Client used to make STS requests.
50	stsRequestTimeout = 5 * time.Second
51	// If lifetime left in a cached token is lesser than this value, we fetch a
52	// new one instead of returning the current one.
53	minCachedTokenLifetime = 300 * time.Second
54
55	tokenExchangeGrantType    = "urn:ietf:params:oauth:grant-type:token-exchange"
56	defaultCloudPlatformScope = "https://www.googleapis.com/auth/cloud-platform"
57)
58
59// For overriding in tests.
60var (
61	loadSystemCertPool   = x509.SystemCertPool
62	makeHTTPDoer         = makeHTTPClient
63	readSubjectTokenFrom = ioutil.ReadFile
64	readActorTokenFrom   = ioutil.ReadFile
65	logger               = grpclog.Component("credentials")
66)
67
68// Options configures the parameters used for an STS based token exchange.
69type Options struct {
70	// TokenExchangeServiceURI is the address of the server which implements STS
71	// token exchange functionality.
72	TokenExchangeServiceURI string // Required.
73
74	// Resource is a URI that indicates the target service or resource where the
75	// client intends to use the requested security token.
76	Resource string // Optional.
77
78	// Audience is the logical name of the target service where the client
79	// intends to use the requested security token
80	Audience string // Optional.
81
82	// Scope is a list of space-delimited, case-sensitive strings, that allow
83	// the client to specify the desired scope of the requested security token
84	// in the context of the service or resource where the token will be used.
85	// If this field is left unspecified, a default value of
86	// https://www.googleapis.com/auth/cloud-platform will be used.
87	Scope string // Optional.
88
89	// RequestedTokenType is an identifier, as described in
90	// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
91	// the requested security token.
92	RequestedTokenType string // Optional.
93
94	// SubjectTokenPath is a filesystem path which contains the security token
95	// that represents the identity of the party on behalf of whom the request
96	// is being made.
97	SubjectTokenPath string // Required.
98
99	// SubjectTokenType is an identifier, as described in
100	// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
101	// the security token in the "subject_token_path" parameter.
102	SubjectTokenType string // Required.
103
104	// ActorTokenPath is a  security token that represents the identity of the
105	// acting party.
106	ActorTokenPath string // Optional.
107
108	// ActorTokenType is an identifier, as described in
109	// https://tools.ietf.org/html/rfc8693#section-3, that indicates the type of
110	// the the security token in the "actor_token_path" parameter.
111	ActorTokenType string // Optional.
112}
113
114func (o Options) String() string {
115	return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s:%s", o.TokenExchangeServiceURI, o.Resource, o.Audience, o.Scope, o.RequestedTokenType, o.SubjectTokenPath, o.SubjectTokenType, o.ActorTokenPath, o.ActorTokenType)
116}
117
118// NewCredentials returns a new PerRPCCredentials implementation, configured
119// using opts, which performs token exchange using STS.
120func NewCredentials(opts Options) (credentials.PerRPCCredentials, error) {
121	if err := validateOptions(opts); err != nil {
122		return nil, err
123	}
124
125	// Load the system roots to validate the certificate presented by the STS
126	// endpoint during the TLS handshake.
127	roots, err := loadSystemCertPool()
128	if err != nil {
129		return nil, err
130	}
131
132	return &callCreds{
133		opts:   opts,
134		client: makeHTTPDoer(roots),
135	}, nil
136}
137
138// callCreds provides the implementation of call credentials based on an STS
139// token exchange.
140type callCreds struct {
141	opts   Options
142	client httpDoer
143
144	// Cached accessToken to avoid an STS token exchange for every call to
145	// GetRequestMetadata.
146	mu            sync.Mutex
147	tokenMetadata map[string]string
148	tokenExpiry   time.Time
149}
150
151// GetRequestMetadata returns the cached accessToken, if available and valid, or
152// fetches a new one by performing an STS token exchange.
153func (c *callCreds) GetRequestMetadata(ctx context.Context, _ ...string) (map[string]string, error) {
154	ri, _ := credentials.RequestInfoFromContext(ctx)
155	if err := credentials.CheckSecurityLevel(ri.AuthInfo, credentials.PrivacyAndIntegrity); err != nil {
156		return nil, fmt.Errorf("unable to transfer STS PerRPCCredentials: %v", err)
157	}
158
159	// Holding the lock for the whole duration of the STS request and response
160	// processing ensures that concurrent RPCs don't end up in multiple
161	// requests being made.
162	c.mu.Lock()
163	defer c.mu.Unlock()
164
165	if md := c.cachedMetadata(); md != nil {
166		return md, nil
167	}
168	req, err := constructRequest(ctx, c.opts)
169	if err != nil {
170		return nil, err
171	}
172	respBody, err := sendRequest(c.client, req)
173	if err != nil {
174		return nil, err
175	}
176	ti, err := tokenInfoFromResponse(respBody)
177	if err != nil {
178		return nil, err
179	}
180	c.tokenMetadata = map[string]string{"Authorization": fmt.Sprintf("%s %s", ti.tokenType, ti.token)}
181	c.tokenExpiry = ti.expiryTime
182	return c.tokenMetadata, nil
183}
184
185// RequireTransportSecurity indicates whether the credentials requires
186// transport security.
187func (c *callCreds) RequireTransportSecurity() bool {
188	return true
189}
190
191// httpDoer wraps the single method on the http.Client type that we use. This
192// helps with overriding in unittests.
193type httpDoer interface {
194	Do(req *http.Request) (*http.Response, error)
195}
196
197func makeHTTPClient(roots *x509.CertPool) httpDoer {
198	return &http.Client{
199		Timeout: stsRequestTimeout,
200		Transport: &http.Transport{
201			TLSClientConfig: &tls.Config{
202				RootCAs: roots,
203			},
204		},
205	}
206}
207
208// validateOptions performs the following validation checks on opts:
209// - tokenExchangeServiceURI is not empty
210// - tokenExchangeServiceURI is a valid URI with a http(s) scheme
211// - subjectTokenPath and subjectTokenType are not empty.
212func validateOptions(opts Options) error {
213	if opts.TokenExchangeServiceURI == "" {
214		return errors.New("empty token_exchange_service_uri in options")
215	}
216	u, err := url.Parse(opts.TokenExchangeServiceURI)
217	if err != nil {
218		return err
219	}
220	if u.Scheme != "http" && u.Scheme != "https" {
221		return fmt.Errorf("scheme is not supported: %q. Only http(s) is supported", u.Scheme)
222	}
223
224	if opts.SubjectTokenPath == "" {
225		return errors.New("required field SubjectTokenPath is not specified")
226	}
227	if opts.SubjectTokenType == "" {
228		return errors.New("required field SubjectTokenType is not specified")
229	}
230	return nil
231}
232
233// cachedMetadata returns the cached metadata provided it is not going to
234// expire anytime soon.
235//
236// Caller must hold c.mu.
237func (c *callCreds) cachedMetadata() map[string]string {
238	now := time.Now()
239	// If the cached token has not expired and the lifetime remaining on that
240	// token is greater than the minimum value we are willing to accept, go
241	// ahead and use it.
242	if c.tokenExpiry.After(now) && c.tokenExpiry.Sub(now) > minCachedTokenLifetime {
243		return c.tokenMetadata
244	}
245	return nil
246}
247
248// constructRequest creates the STS request body in JSON based on the provided
249// options.
250// - Contents of the subjectToken are read from the file specified in
251//   options. If we encounter an error here, we bail out.
252// - Contents of the actorToken are read from the file specified in options.
253//   If we encounter an error here, we ignore this field because this is
254//   optional.
255// - Most of the other fields in the request come directly from options.
256//
257// A new HTTP request is created by calling http.NewRequestWithContext() and
258// passing the provided context, thereby enforcing any timeouts specified in
259// the latter.
260func constructRequest(ctx context.Context, opts Options) (*http.Request, error) {
261	subToken, err := readSubjectTokenFrom(opts.SubjectTokenPath)
262	if err != nil {
263		return nil, err
264	}
265	reqScope := opts.Scope
266	if reqScope == "" {
267		reqScope = defaultCloudPlatformScope
268	}
269	reqParams := &requestParameters{
270		GrantType:          tokenExchangeGrantType,
271		Resource:           opts.Resource,
272		Audience:           opts.Audience,
273		Scope:              reqScope,
274		RequestedTokenType: opts.RequestedTokenType,
275		SubjectToken:       string(subToken),
276		SubjectTokenType:   opts.SubjectTokenType,
277	}
278	if opts.ActorTokenPath != "" {
279		actorToken, err := readActorTokenFrom(opts.ActorTokenPath)
280		if err != nil {
281			return nil, err
282		}
283		reqParams.ActorToken = string(actorToken)
284		reqParams.ActorTokenType = opts.ActorTokenType
285	}
286	jsonBody, err := json.Marshal(reqParams)
287	if err != nil {
288		return nil, err
289	}
290	req, err := http.NewRequestWithContext(ctx, "POST", opts.TokenExchangeServiceURI, bytes.NewBuffer(jsonBody))
291	if err != nil {
292		return nil, fmt.Errorf("failed to create http request: %v", err)
293	}
294	req.Header.Set("Content-Type", "application/json")
295	return req, nil
296}
297
298func sendRequest(client httpDoer, req *http.Request) ([]byte, error) {
299	// http.Client returns a non-nil error only if it encounters an error
300	// caused by client policy (such as CheckRedirect), or failure to speak
301	// HTTP (such as a network connectivity problem). A non-2xx status code
302	// doesn't cause an error.
303	resp, err := client.Do(req)
304	if err != nil {
305		return nil, err
306	}
307
308	// When the http.Client returns a non-nil error, it is the
309	// responsibility of the caller to read the response body till an EOF is
310	// encountered and to close it.
311	body, err := ioutil.ReadAll(resp.Body)
312	resp.Body.Close()
313	if err != nil {
314		return nil, err
315	}
316
317	if resp.StatusCode == http.StatusOK {
318		return body, nil
319	}
320	logger.Warningf("http status %d, body: %s", resp.StatusCode, string(body))
321	return nil, fmt.Errorf("http status %d, body: %s", resp.StatusCode, string(body))
322}
323
324func tokenInfoFromResponse(respBody []byte) (*tokenInfo, error) {
325	respData := &responseParameters{}
326	if err := json.Unmarshal(respBody, respData); err != nil {
327		return nil, fmt.Errorf("json.Unmarshal(%v): %v", respBody, err)
328	}
329	if respData.AccessToken == "" {
330		return nil, fmt.Errorf("empty accessToken in response (%v)", string(respBody))
331	}
332	return &tokenInfo{
333		tokenType:  respData.TokenType,
334		token:      respData.AccessToken,
335		expiryTime: time.Now().Add(time.Duration(respData.ExpiresIn) * time.Second),
336	}, nil
337}
338
339// requestParameters stores all STS request attributes defined in
340// https://tools.ietf.org/html/rfc8693#section-2.1.
341type requestParameters struct {
342	// REQUIRED. The value "urn:ietf:params:oauth:grant-type:token-exchange"
343	// indicates that a token exchange is being performed.
344	GrantType string `json:"grant_type"`
345	// OPTIONAL. Indicates the location of the target service or resource where
346	// the client intends to use the requested security token.
347	Resource string `json:"resource,omitempty"`
348	// OPTIONAL. The logical name of the target service where the client intends
349	// to use the requested security token.
350	Audience string `json:"audience,omitempty"`
351	// OPTIONAL. A list of space-delimited, case-sensitive strings, that allow
352	// the client to specify the desired scope of the requested security token
353	// in the context of the service or Resource where the token will be used.
354	Scope string `json:"scope,omitempty"`
355	// OPTIONAL. An identifier, for the type of the requested security token.
356	RequestedTokenType string `json:"requested_token_type,omitempty"`
357	// REQUIRED. A security token that represents the identity of the party on
358	// behalf of whom the request is being made.
359	SubjectToken string `json:"subject_token"`
360	// REQUIRED. An identifier, that indicates the type of the security token in
361	// the "subject_token" parameter.
362	SubjectTokenType string `json:"subject_token_type"`
363	// OPTIONAL. A security token that represents the identity of the acting
364	// party.
365	ActorToken string `json:"actor_token,omitempty"`
366	// An identifier, that indicates the type of the security token in the
367	// "actor_token" parameter.
368	ActorTokenType string `json:"actor_token_type,omitempty"`
369}
370
371// nesponseParameters stores all attributes sent as JSON in a successful STS
372// response. These attributes are defined in
373// https://tools.ietf.org/html/rfc8693#section-2.2.1.
374type responseParameters struct {
375	// REQUIRED. The security token issued by the authorization server
376	// in response to the token exchange request.
377	AccessToken string `json:"access_token"`
378	// REQUIRED. An identifier, representation of the issued security token.
379	IssuedTokenType string `json:"issued_token_type"`
380	// REQUIRED. A case-insensitive value specifying the method of using the access
381	// token issued. It provides the client with information about how to utilize the
382	// access token to access protected resources.
383	TokenType string `json:"token_type"`
384	// RECOMMENDED. The validity lifetime, in seconds, of the token issued by the
385	// authorization server.
386	ExpiresIn int64 `json:"expires_in"`
387	// OPTIONAL, if the Scope of the issued security token is identical to the
388	// Scope requested by the client; otherwise, REQUIRED.
389	Scope string `json:"scope"`
390	// OPTIONAL. A refresh token will typically not be issued when the exchange is
391	// of one temporary credential (the subject_token) for a different temporary
392	// credential (the issued token) for use in some other context.
393	RefreshToken string `json:"refresh_token"`
394}
395
396// tokenInfo wraps the information received in a successful STS response.
397type tokenInfo struct {
398	tokenType  string
399	token      string
400	expiryTime time.Time
401}
402