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