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