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