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 dpopAuthserverNonce := "" 258 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey) 259 if err != nil { 260 return nil, fmt.Errorf("error getting dpop proof: %w", err) 261 } 262 263 params := url.Values{ 264 "response_type": {"code"}, 265 "code_challenge": {codeChallenge}, 266 "code_challenge_method": {codeChallengeMethod}, 267 "client_id": {c.clientId}, 268 "state": {state}, 269 "redirect_uri": {c.redirectUri}, 270 "scope": {scope}, 271 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 272 "client_assertion": {clientAssertion}, 273 } 274 275 if loginHint != "" { 276 params.Set("login_hint", loginHint) 277 } 278 279 _, err = isSafeAndParsed(parUrl) 280 if err != nil { 281 return nil, err 282 } 283 284 req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode())) 285 if err != nil { 286 return nil, err 287 } 288 289 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 290 req.Header.Set("DPoP", dpopProof) 291 292 resp, err := c.h.Do(req) 293 if err != nil { 294 return nil, err 295 } 296 defer resp.Body.Close() 297 298 var rmap map[string]any 299 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 300 return nil, err 301 } 302 303 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" { 304 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce") 305 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey) 306 if err != nil { 307 return nil, err 308 } 309 310 req2, err := http.NewRequestWithContext( 311 ctx, 312 "POST", 313 parUrl, 314 strings.NewReader(params.Encode()), 315 ) 316 if err != nil { 317 return nil, err 318 } 319 320 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") 321 req2.Header.Set("DPoP", dpopProof) 322 323 resp2, err := c.h.Do(req2) 324 if err != nil { 325 return nil, err 326 } 327 defer resp2.Body.Close() 328 329 rmap = map[string]any{} 330 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil { 331 return nil, err 332 } 333 334 fmt.Println(rmap) 335 } 336 337 return &SendParAuthResponse{ 338 PkceVerifier: pkceVerifier, 339 State: state, 340 DpopAuthserverNonce: dpopAuthserverNonce, 341 Resp: rmap, 342 }, nil 343} 344 345type TokenResponse struct { 346 DpopAuthserverNonce string 347 Resp map[string]any 348} 349 350func (c *OauthClient) InitialTokenRequest( 351 ctx context.Context, 352 code, 353 appUrl, 354 authserverIss, 355 pkceVerifier, 356 dpopAuthserverNonce string, 357 dpopPrivateJwk jwk.Key, 358) (*TokenResponse, error) { 359 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss) 360 if err != nil { 361 return nil, err 362 } 363 364 clientAssertion, err := c.ClientAssertionJwt(authserverIss) 365 if err != nil { 366 return nil, err 367 } 368 369 params := url.Values{ 370 "client_id": {c.clientId}, 371 "redirect_uri": {c.redirectUri}, 372 "grant_type": {"authorization_code"}, 373 "code": {code}, 374 "code_verifier": {pkceVerifier}, 375 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 376 "client_assertion": {clientAssertion}, 377 } 378 379 dpopProof, err := c.AuthServerDpopJwt( 380 "POST", 381 authserverMeta.TokenEndpoint, 382 dpopAuthserverNonce, 383 dpopPrivateJwk, 384 ) 385 if err != nil { 386 return nil, err 387 } 388 389 req, err := http.NewRequestWithContext( 390 ctx, 391 "POST", 392 authserverMeta.TokenEndpoint, 393 strings.NewReader(params.Encode()), 394 ) 395 if err != nil { 396 return nil, err 397 } 398 399 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 400 req.Header.Set("DPoP", dpopProof) 401 402 resp, err := c.h.Do(req) 403 if err != nil { 404 return nil, err 405 } 406 defer resp.Body.Close() 407 408 // TODO: use nonce if needed, same as in par 409 410 var rmap map[string]any 411 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 412 return nil, err 413 } 414 415 return &TokenResponse{ 416 DpopAuthserverNonce: dpopAuthserverNonce, 417 Resp: rmap, 418 }, nil 419} 420 421type RefreshTokenArgs struct { 422 AuthserverUrl string 423 RefreshToken string 424 DpopPrivateJwk string 425 DpopAuthserverNonce string 426} 427 428func (c *OauthClient) RefreshTokenRequest( 429 ctx context.Context, 430 args RefreshTokenArgs, 431 appUrl string, 432) (any, error) { 433 authserverMeta, err := c.FetchAuthServerMetadata(ctx, args.AuthserverUrl) 434 if err != nil { 435 return nil, err 436 } 437 438 clientAssertion, err := c.ClientAssertionJwt(args.AuthserverUrl) 439 if err != nil { 440 return nil, err 441 } 442 443 params := url.Values{ 444 "client_id": {c.clientId}, 445 "grant_type": {"refresh_token"}, 446 "refresh_token": {args.RefreshToken}, 447 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 448 "client_assertion": {clientAssertion}, 449 } 450 451 dpopPrivateJwk, err := parsePrivateJwkFromString(args.DpopPrivateJwk) 452 if err != nil { 453 return nil, err 454 } 455 456 dpopProof, err := c.AuthServerDpopJwt( 457 "POST", 458 authserverMeta.TokenEndpoint, 459 args.DpopAuthserverNonce, 460 dpopPrivateJwk, 461 ) 462 if err != nil { 463 return nil, err 464 } 465 466 req, err := http.NewRequestWithContext( 467 ctx, 468 "POST", 469 authserverMeta.TokenEndpoint, 470 strings.NewReader(params.Encode()), 471 ) 472 if err != nil { 473 return nil, err 474 } 475 476 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 477 req.Header.Set("DPoP", dpopProof) 478 479 resp, err := c.h.Do(req) 480 if err != nil { 481 return nil, err 482 } 483 defer resp.Body.Close() 484 485 // TODO: handle same thing as above... 486 487 if resp.StatusCode != 200 && resp.StatusCode != 201 { 488 b, _ := io.ReadAll(resp.Body) 489 return nil, fmt.Errorf("token refresh error: %s", string(b)) 490 } 491 492 var rmap map[string]any 493 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 494 return nil, err 495 } 496 497 return &TokenResponse{ 498 DpopAuthserverNonce: args.DpopAuthserverNonce, 499 Resp: rmap, 500 }, nil 501} 502 503func generateToken(len int) (string, error) { 504 b := make([]byte, len) 505 if _, err := rand.Read(b); err != nil { 506 return "", err 507 } 508 509 return hex.EncodeToString(b), nil 510} 511 512func generateCodeChallenge(pkceVerifier string) string { 513 h := sha256.New() 514 h.Write([]byte(pkceVerifier)) 515 hash := h.Sum(nil) 516 return base64.RawURLEncoding.EncodeToString(hash) 517} 518 519func parsePrivateJwkFromString(str string) (jwk.Key, error) { 520 return jwk.ParseKey([]byte(str)) 521}