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}