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  }