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(method, url, nonce string, privateJwk jwk.Key) (string, error) {
178 pubJwk, err := privateJwk.PublicKey()
179 if err != nil {
180 return "", err
181 }
182
183 b, err := json.Marshal(pubJwk)
184 if err != nil {
185 return "", err
186 }
187
188 var pubMap map[string]any
189 if err := json.Unmarshal(b, &pubMap); err != nil {
190 return "", err
191 }
192
193 now := time.Now().Unix()
194
195 claims := jwt.MapClaims{
196 "jti": uuid.NewString(),
197 "htm": method,
198 "htu": url,
199 "iat": now,
200 "exp": now + 30,
201 }
202
203 if nonce != "" {
204 claims["nonce"] = nonce
205 }
206
207 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
208 token.Header["typ"] = "dpop+jwt"
209 token.Header["alg"] = "ES256"
210 token.Header["jwk"] = pubMap
211
212 var rawKey any
213 if err := privateJwk.Raw(&rawKey); err != nil {
214 return "", err
215 }
216
217 tokenString, err := token.SignedString(rawKey)
218 if err != nil {
219 return "", fmt.Errorf("failed to sign token: %w", err)
220 }
221
222 return tokenString, nil
223}
224
225type SendParAuthResponse struct {
226 PkceVerifier string
227 State string
228 DpopAuthserverNonce string
229 Resp map[string]any
230}
231
232func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
233 if authServerMeta == nil {
234 return nil, fmt.Errorf("nil metadata provided")
235 }
236
237 parUrl := authServerMeta.PushedAuthorizationRequestEndpoint
238
239 state, err := generateToken(10)
240 if err != nil {
241 return nil, fmt.Errorf("could not generate state token: %w", err)
242 }
243
244 pkceVerifier, err := generateToken(48)
245 if err != nil {
246 return nil, fmt.Errorf("could not generate pkce verifier: %w", err)
247 }
248
249 codeChallenge := generateCodeChallenge(pkceVerifier)
250 codeChallengeMethod := "S256"
251
252 clientAssertion, err := c.ClientAssertionJwt(authServerUrl)
253 if err != nil {
254 return nil, err
255 }
256
257 // TODO: ??
258 dpopAuthserverNonce := ""
259 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
260 if err != nil {
261 return nil, fmt.Errorf("error getting dpop proof: %w", err)
262 }
263
264 params := url.Values{
265 "response_type": {"code"},
266 "code_challenge": {codeChallenge},
267 "code_challenge_method": {codeChallengeMethod},
268 "client_id": {c.clientId},
269 "state": {state},
270 "redirect_uri": {c.redirectUri},
271 "scope": {scope},
272 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
273 "client_assertion": {clientAssertion},
274 }
275
276 if loginHint != "" {
277 params.Set("login_hint", loginHint)
278 }
279
280 _, err = isSafeAndParsed(parUrl)
281 if err != nil {
282 return nil, err
283 }
284
285 req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
286 if err != nil {
287 return nil, err
288 }
289
290 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
291 req.Header.Set("DPoP", dpopProof)
292
293 resp, err := c.h.Do(req)
294 if err != nil {
295 return nil, err
296 }
297 defer resp.Body.Close()
298
299 var rmap map[string]any
300 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
301 return nil, err
302 }
303
304 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" {
305 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
306 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
307 if err != nil {
308 return nil, err
309 }
310
311 req2, err := http.NewRequestWithContext(
312 ctx,
313 "POST",
314 parUrl,
315 strings.NewReader(params.Encode()),
316 )
317 if err != nil {
318 return nil, err
319 }
320
321 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
322 req2.Header.Set("DPoP", dpopProof)
323
324 resp2, err := c.h.Do(req2)
325 if err != nil {
326 return nil, err
327 }
328 defer resp2.Body.Close()
329
330 rmap = map[string]any{}
331 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil {
332 return nil, err
333 }
334
335 fmt.Println(rmap)
336 }
337
338 return &SendParAuthResponse{
339 PkceVerifier: pkceVerifier,
340 State: state,
341 DpopAuthserverNonce: dpopAuthserverNonce,
342 Resp: rmap,
343 }, nil
344}
345
346type TokenResponse struct {
347 DpopAuthserverNonce string
348 Resp map[string]any
349}
350
351func (c *OauthClient) InitialTokenRequest(
352 ctx context.Context,
353 code,
354 appUrl,
355 authserverIss,
356 pkceVerifier,
357 dpopAuthserverNonce string,
358 dpopPrivateJwk jwk.Key,
359) (*TokenResponse, error) {
360 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss)
361 if err != nil {
362 return nil, err
363 }
364
365 clientAssertion, err := c.ClientAssertionJwt(authserverIss)
366 if err != nil {
367 return nil, err
368 }
369
370 params := url.Values{
371 "client_id": {c.clientId},
372 "redirect_uri": {c.redirectUri},
373 "grant_type": {"authorization_code"},
374 "code": {code},
375 "code_verifier": {pkceVerifier},
376 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
377 "client_assertion": {clientAssertion},
378 }
379
380 dpopProof, err := c.AuthServerDpopJwt(
381 "POST",
382 authserverMeta.TokenEndpoint,
383 dpopAuthserverNonce,
384 dpopPrivateJwk,
385 )
386 if err != nil {
387 return nil, err
388 }
389
390 req, err := http.NewRequestWithContext(
391 ctx,
392 "POST",
393 authserverMeta.TokenEndpoint,
394 strings.NewReader(params.Encode()),
395 )
396 if err != nil {
397 return nil, err
398 }
399
400 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
401 req.Header.Set("DPoP", dpopProof)
402
403 resp, err := c.h.Do(req)
404 if err != nil {
405 return nil, err
406 }
407 defer resp.Body.Close()
408
409 // TODO: use nonce if needed, same as in par
410
411 var rmap map[string]any
412 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
413 return nil, err
414 }
415
416 return &TokenResponse{
417 DpopAuthserverNonce: dpopAuthserverNonce,
418 Resp: rmap,
419 }, nil
420}
421
422type RefreshTokenArgs struct {
423 AuthserverUrl string
424 RefreshToken string
425 DpopPrivateJwk string
426 DpopAuthserverNonce string
427}
428
429func (c *OauthClient) RefreshTokenRequest(
430 ctx context.Context,
431 args RefreshTokenArgs,
432 appUrl string,
433) (any, error) {
434 authserverMeta, err := c.FetchAuthServerMetadata(ctx, args.AuthserverUrl)
435 if err != nil {
436 return nil, err
437 }
438
439 clientAssertion, err := c.ClientAssertionJwt(args.AuthserverUrl)
440 if err != nil {
441 return nil, err
442 }
443
444 params := url.Values{
445 "client_id": {c.clientId},
446 "grant_type": {"refresh_token"},
447 "refresh_token": {args.RefreshToken},
448 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
449 "client_assertion": {clientAssertion},
450 }
451
452 dpopPrivateJwk, err := parsePrivateJwkFromString(args.DpopPrivateJwk)
453 if err != nil {
454 return nil, err
455 }
456
457 dpopProof, err := c.AuthServerDpopJwt(
458 "POST",
459 authserverMeta.TokenEndpoint,
460 args.DpopAuthserverNonce,
461 dpopPrivateJwk,
462 )
463 if err != nil {
464 return nil, err
465 }
466
467 req, err := http.NewRequestWithContext(
468 ctx,
469 "POST",
470 authserverMeta.TokenEndpoint,
471 strings.NewReader(params.Encode()),
472 )
473 if err != nil {
474 return nil, err
475 }
476
477 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
478 req.Header.Set("DPoP", dpopProof)
479
480 resp, err := c.h.Do(req)
481 if err != nil {
482 return nil, err
483 }
484 defer resp.Body.Close()
485
486 // TODO: handle same thing as above...
487
488 if resp.StatusCode != 200 && resp.StatusCode != 201 {
489 b, _ := io.ReadAll(resp.Body)
490 return nil, fmt.Errorf("token refresh error: %s", string(b))
491 }
492
493 var rmap map[string]any
494 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
495 return nil, err
496 }
497
498 return &TokenResponse{
499 DpopAuthserverNonce: args.DpopAuthserverNonce,
500 Resp: rmap,
501 }, nil
502}
503
504func generateToken(len int) (string, error) {
505 b := make([]byte, len)
506 if _, err := rand.Read(b); err != nil {
507 return "", err
508 }
509
510 return hex.EncodeToString(b), nil
511}
512
513func generateCodeChallenge(pkceVerifier string) string {
514 h := sha256.New()
515 h.Write([]byte(pkceVerifier))
516 hash := h.Sum(nil)
517 return base64.RawURLEncoding.EncodeToString(hash)
518}
519
520func parsePrivateJwkFromString(str string) (jwk.Key, error) {
521 return jwk.ParseKey([]byte(str))
522}