fix(auth): align device-code timeout to the ~15-min code lifetime + document headless/sub-agent sign-in - #38
Merged
Gregory Joseph (gnjoseph) merged 2 commits intoJul 9, 2026
Conversation
…ocument headless/sub-agent sign-in The device-code cancel was hardcoded to 2 min (TTY) / 10 min (headless). The 10-min (600s) headless bound is SHORTER than the ~15-min (900s) Azure AD device-code lifetime, so it prematurely cancelled a sign-in that was still valid. MSAL already stops polling at code expiry, so our client timer only ever needs to be a safety net at that same horizon - never below it. Fix (src/auth.ts): - Replace the fixed, TTY-dependent DEVICE_CODE_TIMEOUT_MS with a bound DERIVED from the device code's real lifetime: DEVICE_CODE_LIFETIME_SECONDS = 900 and a pure deviceCodeCancelDelayMs(expiresIn) helper (seconds -> ms, defaulting to the ~15-min lifetime when expiresIn is unknown). - Set the native DeviceCodeRequest.timeout to the code lifetime; per MSAL "the device code expiration window will always take precedence over this set period", so it caps the wait without cancelling a still-valid code. - Re-arm the JS safety-net timer inside deviceCodeCallback using the STS-reported response.expiresIn, so it can never fire before the code expires. - Keep the existing fail-fast-when-invisible behavior; refresh the stale "hang for up to 10 minutes" comment. Docs (README.md): new "Headless & orchestrator / sub-agent sign-in" section covering interactive defaults + SPE_INTERACTIVE / SPE_NON_INTERACTIVE overrides, why device-code prompts (stderr) are not visible to a calling agent and therefore fail fast instead of hanging, the pre-auth + restart pattern, and the ~15-min device-code timeout. Tests (src/auth.test.ts): assert the cancel horizon is >= the code lifetime and never the old 600s/120s, that expiresIn drives the derivation, and (fake timers) that a still-valid code is not cancelled before it expires. Token/account-selection and guest/B2B logic are untouched. Ref: PR #3 review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Defense-in-depth from PR #3 review: bound the expiresIn-derived cancel delay to 4x the ~15-min AAD code lifetime so an absurd expiresIn can't exceed Node's max setTimeout delay (~24.85 days) and get clamped to 1ms (near-instant cancel). AAD never issues such values and MSAL's native timeout stays authoritative. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Gregory Joseph (gnjoseph)
merged commit Jul 9, 2026
9113292
into
feat/spe-mcp-server
5 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (PR #3 review)
The device-code sign-in cancel was hardcoded:
The 10-min (600s) headless bound is shorter than the ~15-min (900s) Azure AD device-code lifetime, so the client prematurely cancelled a sign-in that was still valid — the user could still have completed it. The 2-min TTY value was likewise arbitrarily short. Two adjacent review questions also asked for the orchestrator/sub-agent behavior and the interactive-vs-headless rationale to be documented.
Fix
src/auth.ts— timeout now derived from the real code lifetime:DEVICE_CODE_LIFETIME_SECONDS = 900(~15 min, the official AAD device-code lifetime);DEVICE_CODE_TIMEOUT_MSis derived from it (no longer TTY-dependent, never below the lifetime).deviceCodeCancelDelayMs(expiresIn?)→ converts the STS-reportedDeviceCodeResponse.expiresIn(seconds) to ms, defaulting to the ~15-min lifetime when not yet known.DeviceCodeRequest.timeoutto the code lifetime. Per MSAL, "the device code expiration window will always take precedence over this set period", so it caps the wait without cancelling a still-valid code.deviceCodeCallbackusingresponse.expiresIn, so it can never fire before the code actually expires."hang for up to 10 minutes"comment.README.md— new "Headless & orchestrator / sub-agent sign-in" section:SPE_INTERACTIVE=1/SPE_NON_INTERACTIVE=1overrides.Why interactive is kept (not removed)
A local SPE app developer benefits from a one-time browser consent (token cached live, no restart); automation gets the opposite default (off). The existing resolution — interactive by default locally, off in headless, explicit overrides — is reasonable, so this PR documents it rather than removing interactive support.
Tests
Added a
device-code sign-in timeoutsuite insrc/auth.test.ts:>=the code lifetime and is not the old600_000/120_000.expiresIndrives the derivation (900 → 900_000,1200 → 1_200_000); invalid/unknown falls back to the ~15-min default.expiresIn = 900, the request's nativetimeoutis900,cancelis stillfalsepast the old 600s point and at 899s, and only flipstrueat the code lifetime.Validation
npm run lint✅ ·npm run typecheck✅ ·npm run build✅npm test✅ — 717 passed | 3 skipped (baseline 713/3; +4 new).Scope / safety
Only the timeout derivation + comments + docs changed. Token/account-selection and guest/B2B logic are untouched. Behavior stays non-blocking / fail-fast where it already was.
Ref: PR #3 review.