sts.go 14 KB

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