fix: prevent workos auth login from hanging indefinitely#139
Conversation
`login.ts` had its own inline device auth polling loop that lacked an AbortController on fetch requests. A hung TCP connection (proxy, captive portal, IPv6 sinkhole) would block `await fetch()` forever, defeating the 5-minute outer timeout. Refactored `runLogin` to use the shared `requestDeviceCode` and `pollForToken` from `device-auth.ts`, which already has a 30-second per-request abort timeout. Also added the same abort timeout to `requestDeviceCode` for the initial device code request. This removes ~100 lines of duplicated logic (JWT parsing, token response handling, sleep helper, endpoint construction, type definitions) that had diverged from `device-auth.ts`.
Greptile SummaryThis PR fixes a hang in Confidence Score: 5/5Safe to merge — one minor P2 style nit, no logic or security regressions introduced. All changes are clean refactors addressing the reported bug. The two previous thread issues (unwrapped AbortError and fragile string matching) are both resolved. Only finding is a redundant No files require special attention. Important Files Changed
|
- Add DeviceAuthTimeoutError subclass so callers can use instanceof instead of fragile string matching on error messages - Wrap AbortError in requestDeviceCode with DeviceAuthTimeoutError instead of letting raw DOMException propagate - Also wrap other fetch errors in DeviceAuthError for consistency
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughRefactors the device-code login flow to delegate device authorization and token polling to Changes
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 60 minutes.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/commands/login.ts`:
- Around line 115-123: The new human-only clack messages around the device auth
flow need JSON branches so --json emits machine-readable output: update the
requestDeviceCode error handling and the status/result blocks (around
requestDeviceCode/deviceAuth and the later 157-177 status/error outputs) to
check the current OutputMode (or the outputMode variable) and, when in JSON
mode, write structured JSON (e.g., { status: "error", error: msg } or { status:
"device_code", data: deviceAuth }) to stdout instead of clack.log.*; keep the
existing clack.log.* for human mode and ensure the same semantic info is present
in both branches using the same identifiers (requestDeviceCode, deviceAuth, and
the login command's status/error variables).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9d9832e9-4469-4dce-aa0f-99ab0fed28c5
📒 Files selected for processing (2)
src/commands/login.tssrc/lib/device-auth.ts
| clack.log.step('Starting authentication...'); | ||
|
|
||
| if (!authResponse.ok) { | ||
| clack.log.error(`Failed to start authentication: ${authResponse.status}`); | ||
| let deviceAuth; | ||
| try { | ||
| deviceAuth = await requestDeviceCode({ clientId, authkitDomain }); | ||
| } catch (error) { | ||
| const msg = error instanceof Error ? error.message : String(error); | ||
| clack.log.error(`Failed to start authentication: ${msg}`); | ||
| process.exit(1); |
There was a problem hiding this comment.
Add JSON-mode handling for the new login status/error output paths.
Line 115, Line 122, and Lines 157-177 introduce/modify human-only clack output without a JSON branch. In --json mode, this can emit non-machine-readable text on the command path you changed.
Suggested direction
- clack.log.step('Starting authentication...');
+ if (isJsonMode()) {
+ console.log(JSON.stringify({ event: 'auth_start' }));
+ } else {
+ clack.log.step('Starting authentication...');
+ }
...
- spinner.stop('Authentication successful!');
- clack.log.success(`Logged in as ${result.email || result.userId}`);
- clack.log.info(`Token expires in ${expiresInSec} seconds`);
+ if (isJsonMode()) {
+ console.log(
+ JSON.stringify({
+ event: 'auth_success',
+ userId: result.userId,
+ email: result.email,
+ expiresInSec,
+ }),
+ );
+ } else {
+ spinner.stop('Authentication successful!');
+ clack.log.success(`Logged in as ${result.email || result.userId}`);
+ clack.log.info(`Token expires in ${expiresInSec} seconds`);
+ }
...
- if (error instanceof DeviceAuthTimeoutError) {
- spinner.stop('Authentication timed out');
- clack.log.error('Authentication timed out. Please try again.');
- } else {
- spinner.stop('Authentication failed');
- const msg = error instanceof Error ? error.message : String(error);
- clack.log.error(`Authentication error: ${msg}`);
- }
+ if (isJsonMode()) {
+ const msg = error instanceof Error ? error.message : String(error);
+ console.log(
+ JSON.stringify({
+ event: 'auth_error',
+ timeout: error instanceof DeviceAuthTimeoutError,
+ message: msg,
+ }),
+ );
+ } else if (error instanceof DeviceAuthTimeoutError) {
+ spinner.stop('Authentication timed out');
+ clack.log.error('Authentication timed out. Please try again.');
+ } else {
+ spinner.stop('Authentication failed');
+ const msg = error instanceof Error ? error.message : String(error);
+ clack.log.error(`Authentication error: ${msg}`);
+ }As per coding guidelines: src/commands/**/*.ts: Implement both human and JSON output modes in commands; check OutputMode usage in src/bin.ts.
Also applies to: 157-177
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/commands/login.ts` around lines 115 - 123, The new human-only clack
messages around the device auth flow need JSON branches so --json emits
machine-readable output: update the requestDeviceCode error handling and the
status/result blocks (around requestDeviceCode/deviceAuth and the later 157-177
status/error outputs) to check the current OutputMode (or the outputMode
variable) and, when in JSON mode, write structured JSON (e.g., { status:
"error", error: msg } or { status: "device_code", data: deviceAuth }) to stdout
instead of clack.log.*; keep the existing clack.log.* for human mode and ensure
the same semantic info is present in both branches using the same identifiers
(requestDeviceCode, deviceAuth, and the login command's status/error variables).
| continue; | ||
| } | ||
| const provisioned = await provisionStagingEnvironment(result.accessToken); | ||
| if (provisioned) { |
| try { | ||
| const result = await pollForToken(deviceAuth.device_code, { | ||
| clientId, | ||
| authkitDomain, |
Summary
workos auth logincould hang forever if a TCP connection stalled (proxy, captive portal, IPv6 sinkhole). The inline polling loop inlogin.tshad noAbortControlleron fetch requests, so a single hung connection defeated the 5-minute outer timeout.login.tsreimplemented the entire device auth polling loop (JWT parsing, token handling, sleep, types) that already existed indevice-auth.ts. The two had diverged --device-auth.tshad a 30-second per-request abort timeout,login.tsdid not. RefactoredrunLoginto callrequestDeviceCode+pollForTokendirectly.requestDeviceCode, which also lacked one.Reported via Slack by Fraser Langton -- CLI stuck on "Waiting for authentication..." after browser showed success.
Test plan
pnpm buildpassespnpm testpasses (1621 tests)pnpm typecheckpassesworkos auth logout && workos auth logincompletes successfullyWORKOS_AUTHKIT_DOMAIN=http://127.0.0.1:9999) aborts within 30s instead of hanging foreverSummary by CodeRabbit