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