本站源代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

186 lines
5.4KB

  1. // Copyright 2014 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package jwt implements the OAuth 2.0 JSON Web Token flow, commonly
  5. // known as "two-legged OAuth 2.0".
  6. //
  7. // See: https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12
  8. package jwt
  9. import (
  10. "context"
  11. "encoding/json"
  12. "fmt"
  13. "io"
  14. "io/ioutil"
  15. "net/http"
  16. "net/url"
  17. "strings"
  18. "time"
  19. "golang.org/x/oauth2"
  20. "golang.org/x/oauth2/internal"
  21. "golang.org/x/oauth2/jws"
  22. )
  23. var (
  24. defaultGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
  25. defaultHeader = &jws.Header{Algorithm: "RS256", Typ: "JWT"}
  26. )
  27. // Config is the configuration for using JWT to fetch tokens,
  28. // commonly known as "two-legged OAuth 2.0".
  29. type Config struct {
  30. // Email is the OAuth client identifier used when communicating with
  31. // the configured OAuth provider.
  32. Email string
  33. // PrivateKey contains the contents of an RSA private key or the
  34. // contents of a PEM file that contains a private key. The provided
  35. // private key is used to sign JWT payloads.
  36. // PEM containers with a passphrase are not supported.
  37. // Use the following command to convert a PKCS 12 file into a PEM.
  38. //
  39. // $ openssl pkcs12 -in key.p12 -out key.pem -nodes
  40. //
  41. PrivateKey []byte
  42. // PrivateKeyID contains an optional hint indicating which key is being
  43. // used.
  44. PrivateKeyID string
  45. // Subject is the optional user to impersonate.
  46. Subject string
  47. // Scopes optionally specifies a list of requested permission scopes.
  48. Scopes []string
  49. // TokenURL is the endpoint required to complete the 2-legged JWT flow.
  50. TokenURL string
  51. // Expires optionally specifies how long the token is valid for.
  52. Expires time.Duration
  53. // Audience optionally specifies the intended audience of the
  54. // request. If empty, the value of TokenURL is used as the
  55. // intended audience.
  56. Audience string
  57. // PrivateClaims optionally specifies custom private claims in the JWT.
  58. // See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
  59. PrivateClaims map[string]interface{}
  60. // UseIDToken optionally specifies whether ID token should be used instead
  61. // of access token when the server returns both.
  62. UseIDToken bool
  63. }
  64. // TokenSource returns a JWT TokenSource using the configuration
  65. // in c and the HTTP client from the provided context.
  66. func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
  67. return oauth2.ReuseTokenSource(nil, jwtSource{ctx, c})
  68. }
  69. // Client returns an HTTP client wrapping the context's
  70. // HTTP transport and adding Authorization headers with tokens
  71. // obtained from c.
  72. //
  73. // The returned client and its Transport should not be modified.
  74. func (c *Config) Client(ctx context.Context) *http.Client {
  75. return oauth2.NewClient(ctx, c.TokenSource(ctx))
  76. }
  77. // jwtSource is a source that always does a signed JWT request for a token.
  78. // It should typically be wrapped with a reuseTokenSource.
  79. type jwtSource struct {
  80. ctx context.Context
  81. conf *Config
  82. }
  83. func (js jwtSource) Token() (*oauth2.Token, error) {
  84. pk, err := internal.ParseKey(js.conf.PrivateKey)
  85. if err != nil {
  86. return nil, err
  87. }
  88. hc := oauth2.NewClient(js.ctx, nil)
  89. claimSet := &jws.ClaimSet{
  90. Iss: js.conf.Email,
  91. Scope: strings.Join(js.conf.Scopes, " "),
  92. Aud: js.conf.TokenURL,
  93. PrivateClaims: js.conf.PrivateClaims,
  94. }
  95. if subject := js.conf.Subject; subject != "" {
  96. claimSet.Sub = subject
  97. // prn is the old name of sub. Keep setting it
  98. // to be compatible with legacy OAuth 2.0 providers.
  99. claimSet.Prn = subject
  100. }
  101. if t := js.conf.Expires; t > 0 {
  102. claimSet.Exp = time.Now().Add(t).Unix()
  103. }
  104. if aud := js.conf.Audience; aud != "" {
  105. claimSet.Aud = aud
  106. }
  107. h := *defaultHeader
  108. h.KeyID = js.conf.PrivateKeyID
  109. payload, err := jws.Encode(&h, claimSet, pk)
  110. if err != nil {
  111. return nil, err
  112. }
  113. v := url.Values{}
  114. v.Set("grant_type", defaultGrantType)
  115. v.Set("assertion", payload)
  116. resp, err := hc.PostForm(js.conf.TokenURL, v)
  117. if err != nil {
  118. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  119. }
  120. defer resp.Body.Close()
  121. body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 1<<20))
  122. if err != nil {
  123. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  124. }
  125. if c := resp.StatusCode; c < 200 || c > 299 {
  126. return nil, &oauth2.RetrieveError{
  127. Response: resp,
  128. Body: body,
  129. }
  130. }
  131. // tokenRes is the JSON response body.
  132. var tokenRes struct {
  133. AccessToken string `json:"access_token"`
  134. TokenType string `json:"token_type"`
  135. IDToken string `json:"id_token"`
  136. ExpiresIn int64 `json:"expires_in"` // relative seconds from now
  137. }
  138. if err := json.Unmarshal(body, &tokenRes); err != nil {
  139. return nil, fmt.Errorf("oauth2: cannot fetch token: %v", err)
  140. }
  141. token := &oauth2.Token{
  142. AccessToken: tokenRes.AccessToken,
  143. TokenType: tokenRes.TokenType,
  144. }
  145. raw := make(map[string]interface{})
  146. json.Unmarshal(body, &raw) // no error checks for optional fields
  147. token = token.WithExtra(raw)
  148. if secs := tokenRes.ExpiresIn; secs > 0 {
  149. token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
  150. }
  151. if v := tokenRes.IDToken; v != "" {
  152. // decode returned id token to get expiry
  153. claimSet, err := jws.Decode(v)
  154. if err != nil {
  155. return nil, fmt.Errorf("oauth2: error decoding JWT token: %v", err)
  156. }
  157. token.Expiry = time.Unix(claimSet.Exp, 0)
  158. }
  159. if js.conf.UseIDToken {
  160. if tokenRes.IDToken == "" {
  161. return nil, fmt.Errorf("oauth2: response doesn't have JWT token")
  162. }
  163. token.AccessToken = tokenRes.IDToken
  164. }
  165. return token, nil
  166. }
上海开阖软件有限公司 沪ICP备12045867号-1