this repo has no description
1package oauth
2
3import (
4 "context"
5 "crypto/ecdsa"
6 "encoding/json"
7 "fmt"
8 "io"
9 "net/http"
10 "net/url"
11 "strings"
12 "time"
13
14 "github.com/golang-jwt/jwt/v5"
15 "github.com/google/uuid"
16 "github.com/lestrrat-go/jwx/v2/jwk"
17)
18
19type Client struct {
20 h *http.Client
21 clientPrivateKey *ecdsa.PrivateKey
22 clientKid string
23 clientId string
24 redirectUri string
25}
26
27type ClientArgs struct {
28 Http *http.Client
29 ClientJwk jwk.Key
30 ClientId string
31 RedirectUri string
32}
33
34func NewClient(args ClientArgs) (*Client, error) {
35 if args.ClientId == "" {
36 return nil, fmt.Errorf("no client id provided")
37 }
38
39 if args.RedirectUri == "" {
40 return nil, fmt.Errorf("no redirect uri provided")
41 }
42
43 if args.Http == nil {
44 args.Http = &http.Client{
45 Timeout: 5 * time.Second,
46 }
47 }
48
49 clientPkey, err := getPrivateKey(args.ClientJwk)
50 if err != nil {
51 return nil, fmt.Errorf("could not load private key from provided client jwk: %w", err)
52 }
53
54 kid := args.ClientJwk.KeyID()
55
56 return &Client{
57 h: args.Http,
58 clientKid: kid,
59 clientPrivateKey: clientPkey,
60 clientId: args.ClientId,
61 redirectUri: args.RedirectUri,
62 }, nil
63}
64
65func (c *Client) ResolvePDSAuthServer(ctx context.Context, ustr string) (string, error) {
66 u, err := isSafeAndParsed(ustr)
67 if err != nil {
68 return "", err
69 }
70
71 u.Path = "/.well-known/oauth-protected-resource"
72
73 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
74 if err != nil {
75 return "", fmt.Errorf("error creating request for oauth protected resource: %w", err)
76 }
77
78 resp, err := c.h.Do(req)
79 if err != nil {
80 return "", fmt.Errorf("could not get response from server: %w", err)
81 }
82 defer resp.Body.Close()
83
84 if resp.StatusCode != http.StatusOK {
85 io.Copy(io.Discard, resp.Body)
86 return "", fmt.Errorf("received non-200 response from pds. code was %d", resp.StatusCode)
87 }
88
89 b, err := io.ReadAll(resp.Body)
90 if err != nil {
91 return "", fmt.Errorf("could not read body: %w", err)
92 }
93
94 var resource OauthProtectedResource
95 if err := resource.UnmarshalJSON(b); err != nil {
96 return "", fmt.Errorf("could not unmarshal json: %w", err)
97 }
98
99 if len(resource.AuthorizationServers) == 0 {
100 return "", fmt.Errorf("oauth protected resource contained no authorization servers")
101 }
102
103 return resource.AuthorizationServers[0], nil
104}
105
106func (c *Client) FetchAuthServerMetadata(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) {
107 u, err := isSafeAndParsed(ustr)
108 if err != nil {
109 return nil, err
110 }
111
112 u.Path = "/.well-known/oauth-authorization-server"
113
114 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
115 if err != nil {
116 return nil, fmt.Errorf("error creating request to fetch auth metadata: %w", err)
117 }
118
119 resp, err := c.h.Do(req)
120 if err != nil {
121 return nil, fmt.Errorf("error getting response for authserver metadata: %w", err)
122 }
123 defer resp.Body.Close()
124
125 if resp.StatusCode != http.StatusOK {
126 io.Copy(io.Discard, resp.Body)
127 return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
128 }
129
130 b, err := io.ReadAll(resp.Body)
131 if err != nil {
132 return nil, fmt.Errorf("could not read body for authserver metadata response: %w", err)
133 }
134
135 var metadata OauthAuthorizationMetadata
136 if err := metadata.UnmarshalJSON(b); err != nil {
137 return nil, fmt.Errorf("could not unmarshal authserver metadata: %w", err)
138 }
139
140 if err := metadata.Validate(u); err != nil {
141 return nil, fmt.Errorf("could not validate authserver metadata: %w", err)
142 }
143
144 return &metadata, nil
145}
146
147func (c *Client) ClientAssertionJwt(authServerUrl string) (string, error) {
148 claims := jwt.MapClaims{
149 "iss": c.clientId,
150 "sub": c.clientId,
151 "aud": authServerUrl,
152 "jti": uuid.NewString(),
153 "iat": time.Now().Unix(),
154 }
155
156 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
157 token.Header["kid"] = c.clientKid
158
159 tokenString, err := token.SignedString(c.clientPrivateKey)
160 if err != nil {
161 return "", err
162 }
163
164 return tokenString, nil
165}
166
167func (c *Client) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) {
168 pubJwk, err := privateJwk.PublicKey()
169 if err != nil {
170 return "", err
171 }
172
173 b, err := json.Marshal(pubJwk)
174 if err != nil {
175 return "", err
176 }
177
178 var pubMap map[string]any
179 if err := json.Unmarshal(b, &pubMap); err != nil {
180 return "", err
181 }
182
183 now := time.Now().Unix()
184
185 claims := jwt.MapClaims{
186 "jti": uuid.NewString(),
187 "htm": method,
188 "htu": url,
189 "iat": now,
190 "exp": now + 30,
191 }
192
193 if nonce != "" {
194 claims["nonce"] = nonce
195 }
196
197 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
198 token.Header["typ"] = "dpop+jwt"
199 token.Header["alg"] = "ES256"
200 token.Header["jwk"] = pubMap
201
202 var rawKey any
203 if err := privateJwk.Raw(&rawKey); err != nil {
204 return "", err
205 }
206
207 tokenString, err := token.SignedString(rawKey)
208 if err != nil {
209 return "", fmt.Errorf("failed to sign token: %w", err)
210 }
211
212 return tokenString, nil
213}
214
215func (c *Client) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
216 if authServerMeta == nil {
217 return nil, fmt.Errorf("nil metadata provided")
218 }
219
220 parUrl := authServerMeta.PushedAuthorizationRequestEndpoint
221
222 state, err := generateToken(10)
223 if err != nil {
224 return nil, fmt.Errorf("could not generate state token: %w", err)
225 }
226
227 pkceVerifier, err := generateToken(48)
228 if err != nil {
229 return nil, fmt.Errorf("could not generate pkce verifier: %w", err)
230 }
231
232 codeChallenge := generateCodeChallenge(pkceVerifier)
233 codeChallengeMethod := "S256"
234
235 clientAssertion, err := c.ClientAssertionJwt(authServerUrl)
236 if err != nil {
237 return nil, err
238 }
239
240 dpopAuthserverNonce := ""
241 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
242 if err != nil {
243 return nil, fmt.Errorf("error getting dpop proof: %w", err)
244 }
245
246 params := url.Values{
247 "response_type": {"code"},
248 "code_challenge": {codeChallenge},
249 "code_challenge_method": {codeChallengeMethod},
250 "client_id": {c.clientId},
251 "state": {state},
252 "redirect_uri": {c.redirectUri},
253 "scope": {scope},
254 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
255 "client_assertion": {clientAssertion},
256 }
257
258 if loginHint != "" {
259 params.Set("login_hint", loginHint)
260 }
261
262 _, err = isSafeAndParsed(parUrl)
263 if err != nil {
264 return nil, err
265 }
266
267 req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
268 if err != nil {
269 return nil, err
270 }
271
272 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
273 req.Header.Set("DPoP", dpopProof)
274
275 resp, err := c.h.Do(req)
276 if err != nil {
277 return nil, err
278 }
279 defer resp.Body.Close()
280
281 var rmap map[string]any
282 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
283 return nil, err
284 }
285
286 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" {
287 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
288 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
289 if err != nil {
290 return nil, err
291 }
292
293 req2, err := http.NewRequestWithContext(
294 ctx,
295 "POST",
296 parUrl,
297 strings.NewReader(params.Encode()),
298 )
299 if err != nil {
300 return nil, err
301 }
302
303 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
304 req2.Header.Set("DPoP", dpopProof)
305
306 resp2, err := c.h.Do(req2)
307 if err != nil {
308 return nil, err
309 }
310 defer resp2.Body.Close()
311
312 rmap = map[string]any{}
313 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil {
314 return nil, err
315 }
316 }
317
318 return &SendParAuthResponse{
319 PkceVerifier: pkceVerifier,
320 State: state,
321 DpopAuthserverNonce: dpopAuthserverNonce,
322 Resp: rmap,
323 }, nil
324}
325
326func (c *Client) InitialTokenRequest(
327 ctx context.Context,
328 code,
329 authserverIss,
330 pkceVerifier,
331 dpopAuthserverNonce string,
332 dpopPrivateJwk jwk.Key,
333) (*TokenResponse, error) {
334 // we might need to re-run to update dpop nonce
335 for range 2 {
336 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
337 if err != nil {
338 return nil, err
339 }
340
341 clientAssertion, err := c.ClientAssertionJwt(authserverIss)
342 if err != nil {
343 return nil, err
344 }
345
346 params := url.Values{
347 "client_id": {c.clientId},
348 "redirect_uri": {c.redirectUri},
349 "grant_type": {"authorization_code"},
350 "code": {code},
351 "code_verifier": {pkceVerifier},
352 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
353 "client_assertion": {clientAssertion},
354 }
355
356 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
357 if err != nil {
358 return nil, err
359 }
360
361 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
362 if err != nil {
363 return nil, err
364 }
365
366 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
367 req.Header.Set("DPoP", dpopProof)
368
369 resp, err := c.h.Do(req)
370 if err != nil {
371 return nil, err
372 }
373 defer resp.Body.Close()
374
375 if resp.StatusCode != 200 && resp.StatusCode != 201 {
376 var respMap map[string]string
377 if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil {
378 return nil, err
379 }
380
381 if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" {
382 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
383 continue
384 }
385
386 return nil, fmt.Errorf("token refresh error: %s", respMap["error"])
387 }
388
389 var tokenResponse TokenResponse
390 if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
391 return nil, err
392 }
393
394 // set nonce so the updates are reflected in the response
395 tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
396
397 return &tokenResponse, nil
398 }
399
400 return nil, nil
401}
402
403func (c *Client) RefreshTokenRequest(
404 ctx context.Context,
405 refreshToken,
406 authserverIss,
407 dpopAuthserverNonce string,
408 dpopPrivateJwk jwk.Key,
409) (*TokenResponse, error) {
410 // we may need to update the dpop nonce
411 for range 2 {
412 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
413 if err != nil {
414 return nil, err
415 }
416
417 clientAssertion, err := c.ClientAssertionJwt(authserverIss)
418 if err != nil {
419 return nil, err
420 }
421
422 params := url.Values{
423 "client_id": {c.clientId},
424 "grant_type": {"refresh_token"},
425 "refresh_token": {refreshToken},
426 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
427 "client_assertion": {clientAssertion},
428 }
429
430 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk)
431 if err != nil {
432 return nil, err
433 }
434
435 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
436 if err != nil {
437 return nil, err
438 }
439
440 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
441 req.Header.Set("DPoP", dpopProof)
442
443 resp, err := c.h.Do(req)
444 if err != nil {
445 return nil, err
446 }
447 defer resp.Body.Close()
448
449 if resp.StatusCode != 200 && resp.StatusCode != 201 {
450 var respMap map[string]string
451 if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil {
452 return nil, err
453 }
454
455 if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" {
456 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
457 continue
458 }
459
460 return nil, fmt.Errorf("token refresh error: %s", respMap["error"])
461 }
462
463 var tokenResponse TokenResponse
464 if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil {
465 return nil, err
466 }
467
468 // set the nonce so that updates are reflected in response
469 tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce
470
471 return &tokenResponse, nil
472 }
473
474 return nil, nil
475}