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, fmt.Errorf("error getting client assertion: %w", 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 != 201 { 287 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" { 288 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce") 289 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey) 290 if err != nil { 291 return nil, err 292 } 293 294 req2, err := http.NewRequestWithContext( 295 ctx, 296 "POST", 297 parUrl, 298 strings.NewReader(params.Encode()), 299 ) 300 if err != nil { 301 return nil, err 302 } 303 304 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") 305 req2.Header.Set("DPoP", dpopProof) 306 307 resp2, err := c.h.Do(req2) 308 if err != nil { 309 return nil, err 310 } 311 defer resp2.Body.Close() 312 313 rmap = map[string]any{} 314 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil { 315 return nil, err 316 } 317 318 if resp2.StatusCode != 201 { 319 return nil, fmt.Errorf("received error from server when submitting par request: %s", rmap["error"]) 320 } 321 } else { 322 return nil, fmt.Errorf("received error from server when submitting par request: %s", rmap["error"]) 323 } 324 } 325 326 return &SendParAuthResponse{ 327 PkceVerifier: pkceVerifier, 328 State: state, 329 DpopAuthserverNonce: dpopAuthserverNonce, 330 ExpiresIn: rmap["expires_in"].(float64), 331 RequestUri: rmap["request_uri"].(string), 332 }, nil 333} 334 335func (c *Client) InitialTokenRequest( 336 ctx context.Context, 337 code, 338 authserverIss, 339 pkceVerifier, 340 dpopAuthserverNonce string, 341 dpopPrivateJwk jwk.Key, 342) (*TokenResponse, error) { 343 // we might need to re-run to update dpop nonce 344 for range 2 { 345 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss) 346 if err != nil { 347 return nil, err 348 } 349 350 clientAssertion, err := c.ClientAssertionJwt(authserverIss) 351 if err != nil { 352 return nil, err 353 } 354 355 params := url.Values{ 356 "client_id": {c.clientId}, 357 "redirect_uri": {c.redirectUri}, 358 "grant_type": {"authorization_code"}, 359 "code": {code}, 360 "code_verifier": {pkceVerifier}, 361 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 362 "client_assertion": {clientAssertion}, 363 } 364 365 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk) 366 if err != nil { 367 return nil, err 368 } 369 370 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode())) 371 if err != nil { 372 return nil, err 373 } 374 375 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 376 req.Header.Set("DPoP", dpopProof) 377 378 resp, err := c.h.Do(req) 379 if err != nil { 380 return nil, err 381 } 382 defer resp.Body.Close() 383 384 if resp.StatusCode != 200 && resp.StatusCode != 201 { 385 var respMap map[string]string 386 if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil { 387 return nil, err 388 } 389 390 if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" { 391 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce") 392 continue 393 } 394 395 return nil, fmt.Errorf("token refresh error: %s", respMap["error"]) 396 } 397 398 var tokenResponse TokenResponse 399 if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { 400 return nil, err 401 } 402 403 // set nonce so the updates are reflected in the response 404 tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce 405 406 return &tokenResponse, nil 407 } 408 409 return nil, nil 410} 411 412func (c *Client) RefreshTokenRequest( 413 ctx context.Context, 414 refreshToken, 415 authserverIss, 416 dpopAuthserverNonce string, 417 dpopPrivateJwk jwk.Key, 418) (*TokenResponse, error) { 419 // we may need to update the dpop nonce 420 for range 2 { 421 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverIss) 422 if err != nil { 423 return nil, err 424 } 425 426 clientAssertion, err := c.ClientAssertionJwt(authserverIss) 427 if err != nil { 428 return nil, err 429 } 430 431 params := url.Values{ 432 "client_id": {c.clientId}, 433 "grant_type": {"refresh_token"}, 434 "refresh_token": {refreshToken}, 435 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 436 "client_assertion": {clientAssertion}, 437 } 438 439 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, dpopAuthserverNonce, dpopPrivateJwk) 440 if err != nil { 441 return nil, err 442 } 443 444 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode())) 445 if err != nil { 446 return nil, err 447 } 448 449 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 450 req.Header.Set("DPoP", dpopProof) 451 452 resp, err := c.h.Do(req) 453 if err != nil { 454 return nil, err 455 } 456 defer resp.Body.Close() 457 458 if resp.StatusCode != 200 && resp.StatusCode != 201 { 459 var respMap map[string]string 460 if err := json.NewDecoder(resp.Body).Decode(&respMap); err != nil { 461 return nil, err 462 } 463 464 if resp.StatusCode == 400 && respMap["error"] == "use_dpop_nonce" { 465 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce") 466 continue 467 } 468 469 return nil, fmt.Errorf("token refresh error: %s", respMap["error"]) 470 } 471 472 var tokenResponse TokenResponse 473 if err := json.NewDecoder(resp.Body).Decode(&tokenResponse); err != nil { 474 return nil, err 475 } 476 477 // set the nonce so that updates are reflected in response 478 tokenResponse.DpopAuthserverNonce = dpopAuthserverNonce 479 480 return &tokenResponse, nil 481 } 482 483 return nil, nil 484}