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