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