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(ctx context.Context, ustr string) (*OauthAuthorizationMetadata, error) { 111 u, err := isSafeAndParsed(ustr) 112 if err != nil { 113 return nil, err 114 } 115 116 u.Path = "/.well-known/oauth-authorization-server" 117 118 req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) 119 if err != nil { 120 return nil, fmt.Errorf("error creating request to fetch auth metadata: %w", err) 121 } 122 123 resp, err := c.h.Do(req) 124 if err != nil { 125 return nil, fmt.Errorf("error getting response for auth metadata: %w", err) 126 } 127 defer resp.Body.Close() 128 129 if resp.StatusCode != http.StatusOK { 130 io.Copy(io.Discard, resp.Body) 131 return nil, fmt.Errorf("received non-200 response from pds. status code was %d", resp.StatusCode) 132 } 133 134 b, err := io.ReadAll(resp.Body) 135 if err != nil { 136 return nil, fmt.Errorf("could not read body for metadata response: %w", err) 137 } 138 139 var metadata OauthAuthorizationMetadata 140 if err := metadata.UnmarshalJSON(b); err != nil { 141 return nil, fmt.Errorf("could not unmarshal metadata: %w", err) 142 } 143 144 if err := metadata.Validate(u); err != nil { 145 return nil, fmt.Errorf("could not validate metadata: %w", err) 146 } 147 148 return &metadata, nil 149} 150 151func (c *OauthClient) ClientAssertionJwt(authServerUrl string) (string, error) { 152 claims := jwt.MapClaims{ 153 "iss": c.clientId, 154 "sub": c.clientId, 155 "aud": authServerUrl, 156 "jti": uuid.NewString(), 157 "iat": time.Now().Unix(), 158 } 159 160 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 161 token.Header["kid"] = c.clientKid 162 163 tokenString, err := token.SignedString(c.clientPrivateKey) 164 if err != nil { 165 return "", err 166 } 167 168 return tokenString, nil 169} 170 171func (c *OauthClient) AuthServerDpopJwt(method, url, nonce string, privateJwk jwk.Key) (string, error) { 172 pubJwk, err := privateJwk.PublicKey() 173 if err != nil { 174 return "", err 175 } 176 177 b, err := json.Marshal(pubJwk) 178 if err != nil { 179 return "", err 180 } 181 182 var pubMap map[string]any 183 if err := json.Unmarshal(b, &pubMap); err != nil { 184 return "", err 185 } 186 187 now := time.Now().Unix() 188 189 claims := jwt.MapClaims{ 190 "jti": uuid.NewString(), 191 "htm": method, 192 "htu": url, 193 "iat": now, 194 "exp": now + 30, 195 } 196 197 if nonce != "" { 198 claims["nonce"] = nonce 199 } 200 201 token := jwt.NewWithClaims(jwt.SigningMethodES256, claims) 202 token.Header["typ"] = "dpop+jwt" 203 token.Header["alg"] = "ES256" 204 token.Header["jwk"] = pubMap 205 206 var rawKey any 207 if err := privateJwk.Raw(&rawKey); err != nil { 208 return "", err 209 } 210 211 tokenString, err := token.SignedString(rawKey) 212 if err != nil { 213 return "", fmt.Errorf("failed to sign token: %w", err) 214 } 215 216 return tokenString, nil 217} 218 219type SendParAuthResponse struct { 220 PkceVerifier string 221 State string 222 DpopAuthserverNonce string 223 Resp map[string]any 224} 225 226func (c *OauthClient) SendParAuthRequest(ctx context.Context, authServerUrl string, authServerMeta *OauthAuthorizationMetadata, loginHint, scope string, dpopPrivateKey jwk.Key) (*SendParAuthResponse, error) { 227 if authServerMeta == nil { 228 return nil, fmt.Errorf("nil metadata provided") 229 } 230 231 parUrl := authServerMeta.PushedAuthorizationRequestEndpoint 232 233 state, err := generateToken(10) 234 if err != nil { 235 return nil, fmt.Errorf("could not generate state token: %w", err) 236 } 237 238 pkceVerifier, err := generateToken(48) 239 if err != nil { 240 return nil, fmt.Errorf("could not generate pkce verifier: %w", err) 241 } 242 243 codeChallenge := generateCodeChallenge(pkceVerifier) 244 codeChallengeMethod := "S256" 245 246 clientAssertion, err := c.ClientAssertionJwt(authServerUrl) 247 if err != nil { 248 return nil, err 249 } 250 251 // TODO: ?? 252 dpopAuthserverNonce := "" 253 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey) 254 if err != nil { 255 return nil, fmt.Errorf("error getting dpop proof: %w", err) 256 } 257 258 params := url.Values{ 259 "response_type": {"code"}, 260 "code_challenge": {codeChallenge}, 261 "code_challenge_method": {codeChallengeMethod}, 262 "client_id": {c.clientId}, 263 "state": {state}, 264 "redirect_uri": {c.redirectUri}, 265 "scope": {scope}, 266 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 267 "client_assertion": {clientAssertion}, 268 } 269 270 if loginHint != "" { 271 params.Set("login_hint", loginHint) 272 } 273 274 _, err = isSafeAndParsed(parUrl) 275 if err != nil { 276 return nil, err 277 } 278 279 req, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode())) 280 if err != nil { 281 return nil, err 282 } 283 284 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 285 req.Header.Set("DPoP", dpopProof) 286 287 resp, err := c.h.Do(req) 288 if err != nil { 289 return nil, err 290 } 291 defer resp.Body.Close() 292 293 var rmap map[string]any 294 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 295 return nil, err 296 } 297 298 if resp.StatusCode == 400 && rmap["error"] == "use_dpop_nonce" { 299 dpopAuthserverNonce = resp.Header.Get("DPoP-Nonce") 300 dpopProof, err := c.AuthServerDpopJwt("POST", parUrl, dpopAuthserverNonce, dpopPrivateKey) 301 if err != nil { 302 return nil, err 303 } 304 305 req2, err := http.NewRequestWithContext(ctx, "POST", parUrl, strings.NewReader(params.Encode())) 306 if err != nil { 307 return nil, err 308 } 309 310 req2.Header.Set("Content-Type", "application/x-www-form-urlencoded") 311 req2.Header.Set("DPoP", dpopProof) 312 313 resp2, err := c.h.Do(req2) 314 if err != nil { 315 return nil, err 316 } 317 defer resp2.Body.Close() 318 319 rmap = map[string]any{} 320 if err := json.NewDecoder(resp2.Body).Decode(&rmap); err != nil { 321 return nil, err 322 } 323 } 324 325 return &SendParAuthResponse{ 326 PkceVerifier: pkceVerifier, 327 State: state, 328 DpopAuthserverNonce: dpopAuthserverNonce, 329 Resp: rmap, 330 }, nil 331} 332 333type TokenResponse struct { 334 DpopAuthserverNonce string 335 Resp map[string]string 336} 337 338func (c *OauthClient) InitialTokenRequest(ctx context.Context, authRequest map[string]string, code, appUrl string) (*TokenResponse, error) { 339 authserverUrl := authRequest["authserver_iss"] 340 authserverMeta, err := c.FetchAuthServerMetadata(ctx, authserverUrl) 341 if err != nil { 342 return nil, err 343 } 344 345 clientAssertion, err := c.ClientAssertionJwt(authserverUrl) 346 if err != nil { 347 return nil, err 348 } 349 350 params := url.Values{ 351 "client_id": {c.clientId}, 352 "redirect_uri": {c.redirectUri}, 353 "grant_type": {"authorization_code"}, 354 "code": {code}, 355 "code_verifier": {authRequest["pkce_verifier"]}, 356 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 357 "client_assertion": {clientAssertion}, 358 } 359 360 dpopPrivateJwk, err := parsePrivateJwkFromString(authRequest["dpop_private_jwk"]) 361 if err != nil { 362 return nil, err 363 } 364 365 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, authRequest["dpop_authserver_nonce"], dpopPrivateJwk) 366 if err != nil { 367 return nil, err 368 } 369 370 dpopAuthserverNonce := authRequest["dpop_authserver_nonce"] 371 372 req, err := http.NewRequestWithContext(ctx, "POST", authserverMeta.TokenEndpoint, strings.NewReader(params.Encode())) 373 if err != nil { 374 return nil, err 375 } 376 377 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 378 req.Header.Set("DPoP", dpopProof) 379 380 resp, err := c.h.Do(req) 381 if err != nil { 382 return nil, err 383 } 384 defer resp.Body.Close() 385 386 // TODO: use nonce if needed, same as in par 387 388 var rmap map[string]string 389 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 390 return nil, err 391 } 392 393 return &TokenResponse{ 394 DpopAuthserverNonce: dpopAuthserverNonce, 395 Resp: rmap, 396 }, nil 397} 398 399type RefreshTokenArgs struct { 400 AuthserverUrl string 401 RefreshToken string 402 DpopPrivateJwk string 403 DpopAuthserverNonce string 404} 405 406func (c *OauthClient) RefreshTokenRequest(ctx context.Context, args RefreshTokenArgs, appUrl string) (any, error) { 407 authserverMeta, err := c.FetchAuthServerMetadata(ctx, args.AuthserverUrl) 408 if err != nil { 409 return nil, err 410 } 411 412 clientAssertion, err := c.ClientAssertionJwt(args.AuthserverUrl) 413 if err != nil { 414 return nil, err 415 } 416 417 params := url.Values{ 418 "client_id": {c.clientId}, 419 "grant_type": {"refresh_token"}, 420 "refresh_token": {args.RefreshToken}, 421 "client_assertion_type": {"urn:ietf:params:oauth:client-assertion-type:jwt-bearer"}, 422 "client_assertion": {clientAssertion}, 423 } 424 425 dpopPrivateJwk, err := parsePrivateJwkFromString(args.DpopPrivateJwk) 426 if err != nil { 427 return nil, err 428 } 429 430 dpopProof, err := c.AuthServerDpopJwt("POST", authserverMeta.TokenEndpoint, args.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 // TODO: handle same thing as above... 450 451 if resp.StatusCode != 200 && resp.StatusCode != 201 { 452 b, _ := io.ReadAll(resp.Body) 453 return nil, fmt.Errorf("token refresh error: %s", string(b)) 454 } 455 456 var rmap map[string]string 457 if err := json.NewDecoder(resp.Body).Decode(&rmap); err != nil { 458 return nil, err 459 } 460 461 return &TokenResponse{ 462 DpopAuthserverNonce: args.DpopAuthserverNonce, 463 Resp: rmap, 464 }, nil 465} 466 467func generateToken(len int) (string, error) { 468 b := make([]byte, len) 469 if _, err := rand.Read(b); err != nil { 470 return "", err 471 } 472 473 return hex.EncodeToString(b), nil 474} 475 476func generateCodeChallenge(pkceVerifier string) string { 477 h := sha256.New() 478 h.Write([]byte(pkceVerifier)) 479 hash := h.Sum(nil) 480 return base64.RawURLEncoding.EncodeToString(hash) 481} 482 483func parsePrivateJwkFromString(str string) (jwk.Key, error) { 484 return jwk.ParseKey([]byte(str)) 485}