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