this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve PAR error handling; tidy

+47 -27
+2 -3
atproto/auth/oauth/cmd/oauth-web-demo/main.go
··· 85 85 scopes := []string{"transition:generic"} 86 86 bind := ":8080" 87 87 88 - // TODO: localhost dev mode if hostname is empty 89 88 var config oauth.ClientConfig 90 89 hostname := cctx.String("hostname") 91 90 if hostname == "" { ··· 247 246 248 247 did := s.currentSessionDID(r) 249 248 if did == nil { 250 - // TODO: suppowed to set a WWW header; and could redirect? 249 + // TODO: supposed to set a WWW header; and could redirect? 251 250 http.Error(w, "not authenticated", http.StatusUnauthorized) 252 251 return 253 252 } ··· 303 302 304 303 did := s.currentSessionDID(r) 305 304 if did == nil { 306 - // TODO: suppowed to set a WWW header; and could redirect? 305 + // TODO: supposed to set a WWW header; and could redirect? 307 306 http.Error(w, "not authenticated", http.StatusUnauthorized) 308 307 return 309 308 }
+43 -23
atproto/auth/oauth/oauth.go
··· 185 185 } 186 186 } 187 187 188 - // XXX: refactor this in to ClientAuthStore layer? 188 + // TODO: refactor this in to ClientAuthStore layer? 189 189 priv, err := crypto.ParsePrivateMultibase(sd.DpopPrivateKeyMultibase) 190 190 if err != nil { 191 191 return nil, err ··· 277 277 278 278 // Sends PAR request to auth server 279 279 func (app *ClientApp) SendAuthRequest(ctx context.Context, authMeta *AuthServerMetadata, scope, loginHint string) (*AuthRequestData, error) { 280 - // TODO: pass as argument? 281 - httpClient := http.DefaultClient 282 280 283 281 parURL := authMeta.PushedAuthorizationRequestEndpoint 284 282 state := randomNonce() ··· 287 285 // generate PKCE code challenge for use in PAR request 288 286 codeChallenge := S256CodeChallenge(pkceVerifier) 289 287 290 - slog.Info("preparing PAR", "client_id", app.Config.ClientID, "callback_url", app.Config.CallbackURL) 288 + slog.Debug("preparing PAR", "client_id", app.Config.ClientID, "callback_url", app.Config.CallbackURL) 291 289 body := PushedAuthRequest{ 292 290 ClientID: app.Config.ClientID, 293 291 State: state, ··· 317 315 } 318 316 bodyBytes := []byte(vals.Encode()) 319 317 318 + // when starting a new session, we don't know the DPoP nonce yet 320 319 dpopServerNonce := "" 321 320 322 321 // create new key for the session ··· 325 324 return nil, err 326 325 } 327 326 328 - slog.Info("sending auth request", "scope", scope, "state", state, "redirectURI", app.Config.CallbackURL) 327 + slog.Debug("sending auth request", "scope", scope, "state", state, "redirectURI", app.Config.CallbackURL) 329 328 330 329 var resp *http.Response 331 330 for range 2 { ··· 341 340 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 342 341 req.Header.Set("DPoP", dpopJWT) 343 342 344 - resp, err = httpClient.Do(req) 343 + resp, err = app.Client.Do(req) 345 344 if err != nil { 346 345 return nil, err 347 346 } 348 347 349 - // check if a nonce was provided 348 + // update DPoP Nonce 350 349 dpopServerNonce = resp.Header.Get("DPoP-Nonce") 351 - if resp.StatusCode == 400 && dpopServerNonce != "" { 352 - // TODO: also check that body is JSON with an 'error' string field value of 'use_dpop_nonce' 350 + 351 + // check for an error condition caused by an out of date DPoP nonce 352 + // note that the HTTP status code would be 400 Bad Request on token endpoint, not 401 Unauthorized like it would be on Resource Server requests 353 + if resp.StatusCode == http.StatusBadRequest && dpopServerNonce != "" { 354 + 355 + // parse the error body to confirm the error type 353 356 var errResp map[string]any 354 357 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 355 - slog.Warn("PAR request failed", "authServer", parURL, "err", err, "statusCode", resp.StatusCode) 356 - } else { 357 - slog.Warn("PAR request failed", "authServer", parURL, "resp", errResp, "statusCode", resp.StatusCode) 358 + slog.Warn("PAR request failed, and could not parse response body", "authServer", parURL, "err", err, "statusCode", resp.StatusCode) 359 + resp.Body.Close() 360 + return nil, fmt.Errorf("token refresh failed: HTTP %d", resp.StatusCode) 361 + } else if errResp["error"] != "use_dpop_nonce" { 362 + slog.Warn("PAR request failed", "authServer", parURL, "body", errResp, "statusCode", resp.StatusCode) 363 + return nil, fmt.Errorf("PAR request failed: %s", errResp["error"]) 358 364 } 359 365 360 - // loop around try again 366 + // already updated nonce value above; loop around and try again 367 + // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop 361 368 resp.Body.Close() 362 369 continue 363 370 } 371 + 364 372 // otherwise process result 365 373 break 366 374 } 367 375 368 376 defer resp.Body.Close() 369 - if resp.StatusCode != 200 && resp.StatusCode != 201 { 377 + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { 370 378 var errResp map[string]any 371 379 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 372 380 slog.Warn("PAR request failed", "authServer", parURL, "err", err, "statusCode", resp.StatusCode) ··· 452 460 } 453 461 454 462 // check if a nonce was provided 455 - dpopServerNonce = resp.Header.Get("DPoP-Nonce") 456 - if resp.StatusCode == 400 && dpopServerNonce != "" { 457 - // TODO: also check that body is JSON with an 'error' string field value of 'use_dpop_nonce' 463 + dpopNonceHdr := resp.Header.Get("DPoP-Nonce") 464 + if dpopNonceHdr != "" && dpopNonceHdr != dpopServerNonce { 465 + dpopServerNonce = dpopNonceHdr 466 + } 467 + 468 + // check for an error condition caused by an out of date DPoP nonce 469 + // note that the HTTP status code would be 400 Bad Request on token endpoint, not 401 Unauthorized like it would be on Resource Server requests 470 + if resp.StatusCode == http.StatusBadRequest && dpopNonceHdr != "" { 471 + 472 + // parse the error body to confirm the error type 458 473 var errResp map[string]any 459 474 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 460 - slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode) 461 - } else { 462 - slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "resp", errResp, "statusCode", resp.StatusCode) 475 + slog.Warn("initial token request failed, and could not parse response body", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode) 476 + resp.Body.Close() 477 + return nil, fmt.Errorf("initial token request failed: HTTP %d", resp.StatusCode) 478 + } else if errResp["error"] != "use_dpop_nonce" { 479 + slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "body", errResp, "statusCode", resp.StatusCode) 480 + return nil, fmt.Errorf("initial token request failed: %s", errResp["error"]) 463 481 } 464 482 465 - // loop around try again 483 + // already updated nonce value above; loop around and try again 484 + // NOTE: having already parsed the body means that the error handling below could fail if we call out of 'for' loop 466 485 resp.Body.Close() 467 486 continue 468 487 } 488 + 469 489 // otherwise process result 470 490 break 471 491 } 472 492 473 493 defer resp.Body.Close() 474 - if resp.StatusCode != 200 { 494 + if resp.StatusCode != http.StatusOK { 475 495 var errResp map[string]any 476 496 if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil { 477 497 slog.Warn("initial token request failed", "authServer", authServerMeta.TokenEndpoint, "err", err, "statusCode", resp.StatusCode) ··· 513 533 514 534 // TODO: logger on ClientApp? 515 535 logger := slog.Default().With("did", ident.DID, "handle", ident.Handle, "host", host) 516 - logger.Info("resolving to auth server metadata") 536 + logger.Debug("resolving to auth server metadata") 517 537 authserverURL, err = app.Resolver.ResolveAuthServerURL(ctx, host) 518 538 if err != nil { 519 539 return "", fmt.Errorf("resolving auth server: %w", err)
+2 -1
atproto/auth/oauth/util.go
··· 6 6 "math/rand" 7 7 ) 8 8 9 - // TODO: longer 9 + // this generates pseudo-unique nonces to prevent token (JWT) replay. these do not need to be cryptographically resilient 10 10 func randomNonce() string { 11 + // TODO: make this longer? 11 12 buf := make([]byte, 16) 12 13 rand.Read(buf) 13 14 return base64.RawURLEncoding.EncodeToString(buf)