-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtoken_endpoint.go
More file actions
339 lines (296 loc) · 12.2 KB
/
token_endpoint.go
File metadata and controls
339 lines (296 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package oauthx
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"mime"
"net/http"
"net/url"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/vdbulcke/assert"
"github.com/vdbulcke/oauthx/metric"
"github.com/vdbulcke/oauthx/tracing"
)
// 5.1. Successful Response
//
// The authorization server issues an access token and optional refresh
// token, and constructs the response by adding the following parameters
// to the entity-body of the HTTP response with a 200 (OK) status code:
//
// access_token
// REQUIRED. The access token issued by the authorization server.
//
// token_type
// REQUIRED. The type of the token issued as described in
// Section 7.1. Value is case insensitive.
//
// expires_in
// RECOMMENDED. The lifetime in seconds of the access token. For
// example, the value "3600" denotes that the access token will
// expire in one hour from the time the response was generated.
// If omitted, the authorization server SHOULD provide the
// expiration time via other means or document the default value.
//
// refresh_token
// OPTIONAL. The refresh token, which can be used to obtain new
// access tokens using the same authorization grant as described
// in Section 6.
//
// scope
// OPTIONAL, if identical to the scope requested by the client;
// otherwise, REQUIRED. The scope of the access token as
// described by Section 3.3.
//
// The parameters are included in the entity-body of the HTTP response
// using the "application/json" media type as defined by [RFC4627]. The
// parameters are serialized into a JavaScript Object Notation (JSON)
// structure by adding each parameter at the highest structure level.
// Parameter names and string values are included as JSON strings.
// Numerical values are included as JSON numbers. The order of
// parameters does not matter and can vary.
//
// The authorization server MUST include the HTTP "Cache-Control"
// response header field [RFC2616] with a value of "no-store" in any
// response containing tokens, credentials, or other sensitive
// information, as well as the "Pragma" response header field [RFC2616]
// with a value of "no-cache".
//
// OpenID Connect Core 1.0
// 3.1.3.3. Successful Token Response
// id_token
//
// ID Token value associated with the authenticated session.
type TokenResponse struct {
// access_token
// REQUIRED. The access token issued by the authorization server.
AccessToken string `json:"access_token"`
// token_type
// REQUIRED. The type of the token issued as described in
// Section 7.1. Value is case insensitive.
TokenType string `json:"token_type"`
// expires_in
// RECOMMENDED. The lifetime in seconds of the access token. For
// example, the value "3600" denotes that the access token will
// expire in one hour from the time the response was generated.
// If omitted, the authorization server SHOULD provide the
// expiration time via other means or document the default value.
//
ExpiresIn expirationTime `json:"expires_in,omitempty"`
// refresh_token
// OPTIONAL. The refresh token, which can be used to obtain new
// access tokens using the same authorization grant as described
// in Section 6.
RefreshToken string `json:"refresh_token,omitempty"`
// scope
// OPTIONAL, if identical to the scope requested by the client;
// otherwise, REQUIRED. The scope of the access token as
// described by Section 3.3.
Scope string `json:"scope,omitempty"`
// OpenID Connect Core 1.0
// 3.1.3.3. Successful Token Response
// id_token
// ID Token value associated with the authenticated session.
IDToken string `json:"id_token,omitempty"`
// raw json body
Raw json.RawMessage `json:"-"`
// used to compute expiration
receivedAt time.Time `json:"-"`
}
// SetAuthorizationHeader set "Authorization: Bearer " header with
// access_token from response
func (tr *TokenResponse) SetAuthorizationHeader(req *http.Request) {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tr.AccessToken))
}
func (resp *TokenResponse) GetExpiration() time.Time {
delta := time.Duration(resp.ExpiresIn) * time.Second
return resp.receivedAt.Add(delta)
}
func (resp *TokenResponse) GetTTL() time.Duration {
delta := time.Duration(resp.ExpiresIn) * time.Second
return delta
}
func (t *TokenResponse) UnmarshallPayload(claims any) error {
err := json.Unmarshal(t.Raw, claims)
if err != nil {
return fmt.Errorf("token_response: payload %w", err)
}
return nil
}
// TokenExchangeResponse rfc8693
type TokenExchangeResponse struct {
// access_token
// REQUIRED. The security token issued by the authorization server
// in response to the token exchange request. The "access_token"
// parameter from Section 5.1 of [RFC6749] is used here to carry the
// requested token, which allows this token exchange protocol to use
// the existing OAuth 2.0 request and response constructs defined for
// the token endpoint. The identifier "access_token" is used for
// historical reasons and the issued token need not be an OAuth
// access token.
AccessToken string `json:"access_token"`
// issued_token_type
// REQUIRED. An identifier, as described in Section 3, for the
// representation of the issued security token.
IssuedTokenType string `json:"issued_token_type"`
// token_type
// REQUIRED. A case-insensitive value specifying the method of using
// the access token issued, as specified in Section 7.1 of [RFC6749].
// It provides the client with information about how to utilize the
// access token to access protected resources. For example, a value
// of "Bearer", as specified in [RFC6750], indicates that the issued
// security token is a bearer token and the client can simply present
// it as is without any additional proof of eligibility beyond the
// contents of the token itself. Note that the meaning of this
// parameter is different from the meaning of the "issued_token_type"
// parameter, which declares the representation of the issued
// security token; the term "token type" is more typically used to
// mean the structural or syntactical representation of the security
// token, as it is in all "*_token_type" parameters in this
// specification. If the issued token is not an access token or
// usable as an access token, then the "token_type" value "N_A" is
// used to indicate that an OAuth 2.0 "token_type" identifier is not
// applicable in that context.
TokenType string `json:"token_type"`
// expires_in
// RECOMMENDED. The validity lifetime, in seconds, of the token
// issued by the authorization server. Oftentimes, the client will
// not have the inclination or capability to inspect the content of
// the token, and this parameter provides a consistent and token-
// type-agnostic indication of how long the token can be expected to
// be valid. For example, the value 1800 denotes that the token will
// expire in thirty minutes from the time the response was generated.
ExpiresIn expirationTime `json:"expires_in,omitempty"`
// refresh_token
// OPTIONAL. A refresh token will typically not be issued when the
// exchange is of one temporary credential (the subject_token) for a
// different temporary credential (the issued token) for use in some
// other context. A refresh token can be issued in cases where the
// client of the token exchange needs the ability to access a
// resource even when the original credential is no longer valid
// (e.g., user-not-present or offline scenarios where there is no
// longer any user entertaining an active session with the client).
// Profiles or deployments of this specification should clearly
// document the conditions under which a client should expect a
// refresh token in response to "urn:ietf:params:oauth:grant-
// type:token-exchange" grant type requests.
RefreshToken string `json:"refresh_token,omitempty"`
// scope
// OPTIONAL if the scope of the issued security token is identical to
// the scope requested by the client; otherwise, it is REQUIRED.
Scope string `json:"scope,omitempty"`
}
func (c *OAuthClient) DoTokenRequest(ctx context.Context, tr *TokenRequest) (*TokenResponse, error) {
assert.NotNil(ctx, assert.Panic, "oauth-client: 'ctx' cannot be nil")
assert.NotNil(tr, assert.Panic, "oauth-client: 'TokenRequest' cannot be nil")
params, err := c.PlumbingGenerateTokenRequestParam(tr)
if err != nil {
return nil, err
}
req, err := c.PlumbingNewHttpTokenRequest(params)
if err != nil {
return nil, err
}
return c.PlumbingDoHttpTokenRequest(ctx, req)
}
func (c *OAuthClient) PlumbingGenerateTokenRequestParam(req *TokenRequest) (url.Values, error) {
params := url.Values{}
for _, opt := range req.opts {
opt.SetValue(params)
}
return params, nil
}
func (c *OAuthClient) PlumbingNewHttpTokenRequest(params url.Values) (*http.Request, error) {
assert.NotNil(c.authmethod, assert.Panic, "oauth-client: missing required 'authmethod'")
assert.NotNil(params, assert.Panic, "oauth-client: params cannot be nil")
assert.StrNotEmpty(c.wk.TokenEndpoint, assert.Panic, "oauth-client: missing 'token_endpoint'")
return c.authmethod.NewOAuthAuthenticatedRequest(TokenEndpoint, c.wk.TokenEndpoint, params)
}
func (c *OAuthClient) PlumbingDoHttpTokenRequest(ctx context.Context, req *http.Request) (_ *TokenResponse, err error) {
assert.NotNil(ctx, assert.Panic, "rfc6749: 'ctx' cannot be nil")
assert.NotNil(req, assert.Panic, "rfc6749: 'req' cannot be nil")
endpoint := "token"
timer := prometheus.NewTimer(prometheus.ObserverFunc(func(v float64) {
metric.OAuthDurationHist.WithLabelValues(endpoint).Observe(v)
}))
defer timer.ObserveDuration()
defer metric.DeferMonitorError(endpoint, &err)
tracing.AddHeadersFromContext(ctx, req)
resp, err := c.http.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, c.http.maxSizeBytes))
if err != nil {
err = fmt.Errorf("rfc6749: %w", err)
return nil, err
}
httpErr := &HttpErr{
StatusCode: resp.StatusCode,
RespBody: body,
ResponseHeader: resp.Header,
}
if len(body) >= int(c.http.maxSizeBytes) {
err = fmt.Errorf("http-limit: http resp body max size limit exceeded: %d bytes", c.http.maxSizeBytes)
httpErr.Err = err
return nil, httpErr
}
// The authorization server issues an access token and optional refresh
// token, and constructs the response by adding the following parameters
// to the entity-body of the HTTP response with a 200 (OK) status code:
if resp.StatusCode != http.StatusOK {
httpErr.Err = fmt.Errorf("rfc6749: expected status code 200 but got '%d'", resp.StatusCode)
err = httpErr
return nil, err
}
// The parameters are included in the entity-body of the HTTP response
// using the "application/json" media type as defined by [RFC4627]. The
// parameters are serialized into a JavaScript Object Notation (JSON)
// structure by adding each parameter at the highest structure level.
// Parameter names and string values are included as JSON strings.
// Numerical values are included as JSON numbers. The order of
// parameters does not matter and can vary.
ct, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
if ct != "application/json" {
httpErr.Err = fmt.Errorf("rfc6749: expected Content-Type 'application/json' but got '%s'", ct)
err = httpErr
return nil, err
}
var raw json.RawMessage
err = json.Unmarshal(body, &raw)
if err != nil {
return nil, fmt.Errorf("rfc6749: invalid json %w", err)
}
var tokenResp TokenResponse
err = json.Unmarshal(body, &tokenResp)
if err != nil {
return nil, fmt.Errorf("rfc6749: invalid json %w", err)
}
tokenResp.receivedAt = TimeFunc()
tokenResp.Raw = raw
return &tokenResp, nil
}
// from golang.org/x/oauth2
type expirationTime int32
func (e *expirationTime) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "null" {
return nil
}
var n json.Number
err := json.Unmarshal(b, &n)
if err != nil {
return err
}
i, err := n.Int64()
if err != nil {
return err
}
if i > math.MaxInt32 {
i = math.MaxInt32
}
*e = expirationTime(i)
return nil
}