Skip to content

fix: prevent workos auth login from hanging indefinitely#139

Merged
nicknisi merged 3 commits intomainfrom
fix/login-auth-hang
Apr 30, 2026
Merged

fix: prevent workos auth login from hanging indefinitely#139
nicknisi merged 3 commits intomainfrom
fix/login-auth-hang

Conversation

@nicknisi
Copy link
Copy Markdown
Member

@nicknisi nicknisi commented Apr 30, 2026

Summary

  • Fixes auth hang: workos auth login could hang forever if a TCP connection stalled (proxy, captive portal, IPv6 sinkhole). The inline polling loop in login.ts had no AbortController on fetch requests, so a single hung connection defeated the 5-minute outer timeout.
  • Deduplicates device auth: login.ts reimplemented the entire device auth polling loop (JWT parsing, token handling, sleep, types) that already existed in device-auth.ts. The two had diverged -- device-auth.ts had a 30-second per-request abort timeout, login.ts did not. Refactored runLogin to call requestDeviceCode + pollForToken directly.
  • Abort on initial request too: Added the same 30-second abort timeout to requestDeviceCode, which also lacked one.
  • Net -101 lines: Removed ~158 lines of duplicated logic, added ~57 lines of integration code.

Reported via Slack by Fraser Langton -- CLI stuck on "Waiting for authentication..." after browser showed success.

Test plan

  • pnpm build passes
  • pnpm test passes (1621 tests)
  • pnpm typecheck passes
  • Manual test: workos auth logout && workos auth login completes successfully
  • Manual test: pointing CLI at a hanging server (WORKOS_AUTHKIT_DOMAIN=http://127.0.0.1:9999) aborts within 30s instead of hanging forever

Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes & Improvements
    • Centralized device authorization and token polling for a more reliable login flow.
    • Improved timeout detection with a distinct timeout response and clearer messaging.
    • Consolidated error handling during polling to reduce confusing failures and exits.
    • Credential persistence updated to a normalized format for more consistent post-login state.

`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-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR fixes a hang in workos auth login by deduplicating the device-auth polling logic — the old inline implementation in login.ts lacked AbortController on its fetch calls, so a stalled TCP connection could block forever. The refactor delegates entirely to requestDeviceCode / pollForToken in device-auth.ts, which already had per-request 30-second timeouts on poll requests and now also has one on the initial device-code request, and introduces a typed DeviceAuthTimeoutError subclass so callers can distinguish timeout from other failures.

Confidence Score: 5/5

Safe 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 clearTimeout call, which is harmless.

No files require special attention.

Important Files Changed

Filename Overview
src/commands/login.ts Removed ~150 lines of duplicated device-auth logic and replaced with calls to requestDeviceCode + pollForToken; error handling now uses instanceof DeviceAuthTimeoutError instead of fragile string matching.
src/lib/device-auth.ts Adds DeviceAuthTimeoutError subclass, adds 30-second AbortController timeout to requestDeviceCode, and promotes the timeout throw in pollForToken to DeviceAuthTimeoutError; minor redundant clearTimeout in the new catch block.

Sequence Diagram

sequenceDiagram
    participant CLI as login.ts (runLogin)
    participant DA as device-auth.ts
    participant Server as OAuth Server

    CLI->>DA: requestDeviceCode({ clientId, authkitDomain })
    activate DA
    Note over DA: AbortController (30s timeout)
    DA->>Server: POST /oauth2/device_authorization
    alt timeout fires
        DA-->>CLI: throw DeviceAuthTimeoutError
    else HTTP error
        DA-->>CLI: throw DeviceAuthError
    else success
        Server-->>DA: device_code, user_code, verification_uri
        DA-->>CLI: DeviceAuthResponse
    end
    deactivate DA

    CLI->>CLI: show verification_uri, open browser

    CLI->>DA: pollForToken(device_code, { clientId, authkitDomain, interval })
    activate DA
    Note over DA: Outer 5-min timeout loop
    loop every interval seconds
        Note over DA: AbortController (30s per request)
        DA->>Server: POST /oauth2/token
        alt network/abort error
            DA->>DA: log & continue (retry)
        else authorization_pending
            DA->>DA: continue
        else slow_down
            DA->>DA: increase interval, continue
        else fatal error
            DA-->>CLI: throw DeviceAuthError
        else success
            Server-->>DA: access_token, id_token
            DA-->>CLI: DeviceAuthResult
        end
    end
    alt 5-min outer timeout
        DA-->>CLI: throw DeviceAuthTimeoutError
    end
    deactivate DA

    CLI->>CLI: saveCredentials(result)
    CLI->>CLI: provisionStagingEnvironment()
    CLI->>CLI: installSkillsAfterLogin()
Loading

Reviews (3): Last reviewed commit: "chore: formatting" | Re-trigger Greptile

greptile-apps[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional findings.

Open in Devin Review

- 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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 69cf4540-48dd-4269-939e-a155b3a1f7ef

📥 Commits

Reviewing files that changed from the base of the PR and between fa685ec and 95f7bda.

📒 Files selected for processing (1)
  • src/lib/device-auth.ts

📝 Walkthrough

Walkthrough

Refactors the device-code login flow to delegate device authorization and token polling to requestDeviceCode and pollForToken helpers, introduces a DeviceAuthTimeoutError subtype for timeout-specific errors, and updates credential persistence to the helpers' normalized return shape (includes refreshToken).

Changes

Cohort / File(s) Summary
Login Command Refactoring
src/commands/login.ts
Delegates device authorization to requestDeviceCode and token polling to pollForToken; removes local JWT parsing and manual polling/backoff; persists credentials using helper return shape (accessToken, expiresAt, userId, email, refreshToken); consolidates error handling and timeout messaging.
Device Auth Utilities
src/lib/device-auth.ts
Adds export class DeviceAuthTimeoutError extends DeviceAuthError and converts per-request fetches to use AbortController with POLL_REQUEST_TIMEOUT_MS; turns AbortError into DeviceAuthTimeoutError and otherwise wraps errors as DeviceAuthError; pollForToken now throws DeviceAuthTimeoutError for terminal timeouts.

Possibly related PRs

  • fix: improve installer auth recovery #128: Modifies the same device auth helpers (requestDeviceCode, pollForToken) and introduces per-request abort/timeouts and a timeout-specific error class; closely related to these changes.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically identifies the main change: preventing indefinite hanging in WorkOS auth login by adding proper request timeouts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/login-auth-hang

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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 4a18de1 and fa685ec.

📒 Files selected for processing (2)
  • src/commands/login.ts
  • src/lib/device-auth.ts

Comment thread src/commands/login.ts
Comment on lines +115 to 123
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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

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).

Comment thread src/commands/login.ts
continue;
}
const provisioned = await provisionStagingEnvironment(result.accessToken);
if (provisioned) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

Comment thread src/commands/login.ts
try {
const result = await pollForToken(deviceAuth.device_code, {
clientId,
authkitDomain,
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test

@nicknisi nicknisi merged commit 1154090 into main Apr 30, 2026
7 checks passed
@nicknisi nicknisi deleted the fix/login-auth-hang branch April 30, 2026 20:19
nicknisi added a commit to nicknisi/diffdad that referenced this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant