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