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(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) {
111 u, err := isSafeAndParsed(ustr)
112 if err != nil {
113 return nil, err
114 }
115
116 u.Path = "/.well-known/oauth-authorization-server"
117
118 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil)
119 if err != nil {
120 return nil, fmt.Errorf("error creating request to fetch auth metadata: %w", err)
121 }
122
123 resp, err := c.h.Do(req)
124 if err != nil {
125 return nil, fmt.Errorf("error getting response for auth metadata: %w", err)
126 }
127 defer resp.Body.Close()
128
129 if resp.StatusCode != http.StatusOK {
130 io.Copy(io.Discard, resp.Body)
131 return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode)
132 }
133
134 b, err := io.ReadAll(resp.Body)
135 if err != nil {
136 return nil, fmt.Errorf("could not read body for metadata response: %w", err)
137 }
138
139 var metadata OauthAuthorizationMetadata
140 if err := metadata.UnmarshalJSON(b); err != nil {
141 return nil, fmt.Errorf("could not unmarshal metadata: %w", err)
142 }
143
144 if err := metadata.Validate(u); err != nil {
145 return nil, fmt.Errorf("could not validate metadata: %w", err)
146 }
147
148 return &metadata, nil
149}
150
151func (c *OauthClient) ClientAssertionJwt(authServerUrl string) (string, error) {
152 claims := jwt.MapClaims{
153 "iss": c.clientId,
154 "sub": c.clientId,
155 "aud": authServerUrl,
156 "jti": uuid.NewString(),
157 "iat": time.Now().Unix(),
158 }
159
160 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
161 token.Header["kid"] = c.clientKid
162
163 tokenString, err := token.SignedString(c.clientPrivateKey)
164 if err != nil {
165 return "", err
166 }
167
168 return tokenString, nil
169}
170
171func (c *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) {
172 pubJwk, err := privateJwk.PublicKey()
173 if err != nil {
174 return "", err
175 }
176
177 b, err := json.Marshal(pubJwk)
178 if err != nil {
179 return "", err
180 }
181
182 var pubMap map[string]any
183 if err := json.Unmarshal(b, &pubMap); err != nil {
184 return "", err
185 }
186
187 now := time.Now().Unix()
188
189 claims := jwt.MapClaims{
190 "jti": uuid.NewString(),
191 "htm": method,
192 "htu": url,
193 "iat": now,
194 "exp": now + 30,
195 }
196
197 if nonce != "" {
198 claims["nonce"] = nonce
199 }
200
201 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
202 token.Header["typ"] = "dpop+jwt"
203 token.Header["alg"] = "ES256"
204 token.Header["jwk"] = pubMap
205
206 var rawKey any
207 if err := privateJwk.Raw(&rawKey); err != nil {
208 return "", err
209 }
210
211 tokenString, err := token.SignedString(rawKey)
212 if err != nil {
213 return "", fmt.Errorf("failed to sign token: %w", err)
214 }
215
216 return tokenString, nil
217}
218
219type SendParAuthResponse struct {
220 PkceVerifier string
221 State string
222 DpopAuthserverNonce string
223 Resp map[string]any
224}
225
226func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) {
227 if authServerMeta == nil {
228 return nil, fmt.Errorf("nil metadata provided")
229 }
230
231 parUrl := authServerMeta.PushedAuthorizationRequestEndpoint
232
233 state, err := generateToken(10)
234 if err != nil {
235 return nil, fmt.Errorf("could not generate state token: %w", err)
236 }
237
238 pkceVerifier, err := generateToken(48)
239 if err != nil {
240 return nil, fmt.Errorf("could not generate pkce verifier: %w", err)
241 }
242
243 codeChallenge := generateCodeChallenge(pkceVerifier)
244 codeChallengeMethod := "S256"
245
246 clientAssertion, err := c.ClientAssertionJwt(authServerUrl)
247 if err != nil {
248 return nil, err
249 }
250
251 // TODO: ??
252 dpopAuthserverNonce := ""
253 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
254 if err != nil {
255 return nil, fmt.Errorf("error getting dpop proof: %w", err)
256 }
257
258 params := url.Values{
259 "response_type": {"code"},
260 "code_challenge": {codeChallenge},
261 "code_challenge_method": {codeChallengeMethod},
262 "client_id": {c.clientId},
263 "state": {state},
264 "redirect_uri": {c.redirectUri},
265 "scope": {scope},
266 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
267 "client_assertion": {clientAssertion},
268 }
269
270 if loginHint != "" {
271 params.Set("login_hint", loginHint)
272 }
273
274 _, err = isSafeAndParsed(parUrl)
275 if err != nil {
276 return nil, err
277 }
278
279 req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
280 if err != nil {
281 return nil, err
282 }
283
284 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
285 req.Header.Set("DPoP", dpopProof)
286
287 resp, err := c.h.Do(req)
288 if err != nil {
289 return nil, err
290 }
291 defer resp.Body.Close()
292
293 var rmap map[string]any
294 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
295 return nil, err
296 }
297
298 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" {
299 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce")
300 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey)
301 if err != nil {
302 return nil, err
303 }
304
305 req2, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode()))
306 if err != nil {
307 return nil, err
308 }
309
310 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded")
311 req2.Header.Set("DPoP", dpopProof)
312
313 resp2, err := c.h.Do(req2)
314 if err != nil {
315 return nil, err
316 }
317 defer resp2.Body.Close()
318
319 rmap = map[string]any{}
320 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil {
321 return nil, err
322 }
323 }
324
325 return &SendParAuthResponse{
326 PkceVerifier: pkceVerifier,
327 State: state,
328 DpopAuthserverNonce: dpopAuthserverNonce,
329 Resp: rmap,
330 }, nil
331}
332
333type TokenResponse struct {
334 DpopAuthserverNonce string
335 Resp map[string]string
336}
337
338func (c *OauthClient) InitialTokenRequest(ctx context.Context, authRequest map[string]string, code, appUrl string) (*TokenResponse, error) {
339 authserverUrl := authRequest["authserver_iss"]
340 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverUrl)
341 if err != nil {
342 return nil, err
343 }
344
345 clientAssertion, err := c.ClientAssertionJwt(authserverUrl)
346 if err != nil {
347 return nil, err
348 }
349
350 params := url.Values{
351 "client_id": {c.clientId},
352 "redirect_uri": {c.redirectUri},
353 "grant_type": {"authorization_code"},
354 "code": {code},
355 "code_verifier": {authRequest["pkce_verifier"]},
356 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
357 "client_assertion": {clientAssertion},
358 }
359
360 dpopPrivateJwk, err := parsePrivateJwkFromString(authRequest["dpop_private_jwk"])
361 if err != nil {
362 return nil, err
363 }
364
365 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, authRequest["dpop_authserver_nonce"], dpopPrivateJwk)
366 if err != nil {
367 return nil, err
368 }
369
370 dpopAuthserverNonce := authRequest["dpop_authserver_nonce"]
371
372 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode()))
373 if err != nil {
374 return nil, err
375 }
376
377 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
378 req.Header.Set("DPoP", dpopProof)
379
380 resp, err := c.h.Do(req)
381 if err != nil {
382 return nil, err
383 }
384 defer resp.Body.Close()
385
386 // TODO: use nonce if needed, same as in par
387
388 var rmap map[string]string
389 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
390 return nil, err
391 }
392
393 return &TokenResponse{
394 DpopAuthserverNonce: dpopAuthserverNonce,
395 Resp: rmap,
396 }, nil
397}
398
399type RefreshTokenArgs struct {
400 AuthserverUrl string
401 RefreshToken string
402 DpopPrivateJwk string
403 DpopAuthserverNonce string
404}
405
406func (c *OauthClient) RefreshTokenRequest(ctx context.Context, args RefreshTokenArgs, appUrl string) (any, error) {
407 authserverMeta, err := c.FetchAuthServerMetadata(ctx, args.AuthserverUrl)
408 if err != nil {
409 return nil, err
410 }
411
412 clientAssertion, err := c.ClientAssertionJwt(args.AuthserverUrl)
413 if err != nil {
414 return nil, err
415 }
416
417 params := url.Values{
418 "client_id": {c.clientId},
419 "grant_type": {"refresh_token"},
420 "refresh_token": {args.RefreshToken},
421 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"},
422 "client_assertion": {clientAssertion},
423 }
424
425 dpopPrivateJwk, err := parsePrivateJwkFromString(args.DpopPrivateJwk)
426 if err != nil {
427 return nil, err
428 }
429
430 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, args.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 // TODO: handle same thing as above...
450
451 if resp.StatusCode != 200 && resp.StatusCode != 201 {
452 b, _ := io.ReadAll(resp.Body)
453 return nil, fmt.Errorf("token refresh error: %s", string(b))
454 }
455
456 var rmap map[string]string
457 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil {
458 return nil, err
459 }
460
461 return &TokenResponse{
462 DpopAuthserverNonce: args.DpopAuthserverNonce,
463 Resp: rmap,
464 }, nil
465}
466
467func generateToken(len int) (string, error) {
468 b := make([]byte, len)
469 if _, err := rand.Read(b); err != nil {
470 return "", err
471 }
472
473 return hex.EncodeToString(b), nil
474}
475
476func generateCodeChallenge(pkceVerifier string) string {
477 h := sha256.New()
478 h.Write([]byte(pkceVerifier))
479 hash := h.Sum(nil)
480 return base64.RawURLEncoding.EncodeToString(hash)
481}
482
483func parsePrivateJwkFromString(str string) (jwk.Key, error) {
484 return jwk.ParseKey([]byte(str))
485}