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