decolua/9router · error

Invalid state parameter

Error message

Invalid state parameter

What it means

Thrown by OAuthService.authenticate() after the callback arrives: the `state` query param returned by the provider does not equal the random `state` generated locally by generatePKCE() at the start of the flow. This is a CSRF/session-mixup guard — the library refuses to proceed because the callback may not belong to this authentication attempt.

Source

Thrown at src/lib/oauth/services/oauth.js:146

    // Start local server and get redirect URI
    const { redirectUri, waitForCallback } = await this.startAuthFlow(null, providerName);

    // Build authorization URL
    const authUrl = buildAuthUrlFn(redirectUri, state, codeChallenge);

    console.log(`\nOpening browser for ${providerName} authentication...`);
    console.log(`If browser doesn't open, visit:\n${authUrl}\n`);

    // Open browser
    await open(authUrl);

    // Wait for callback
    const callbackParams = await waitForCallback();

    // Validate state
    if (callbackParams.state !== state) {
      throw new Error("Invalid state parameter");
    }

    return {
      code: callbackParams.code,
      state: callbackParams.state,
      codeVerifier,
      redirectUri,
    };
  }
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Close all stale tabs from previous auth attempts and restart the flow in a single browser tab.
  2. Run only one authentication flow at a time; concurrent flows can bind the same localhost port and cross-deliver callbacks.
  3. Re-run the flow and complete it promptly (within the 5-minute timeout) without manually editing the URL.
  4. If a proxy strips query params, bypass it for localhost callbacks or complete auth outside the proxied network.

Example fix

// before
const callbackParams = await waitForCallback();
if (callbackParams.state !== state) {
  throw new Error("Invalid state parameter");
}
// after (fail fast with both values for diagnosis)
if (callbackParams.state !== state) {
  throw new Error(`Invalid state parameter: expected ${state}, got ${callbackParams.state}`);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const result = await service.authenticate(provider, buildUrlFn);
} catch (err) {
  if (err.message === "Invalid state parameter") {
    // stale tab or concurrent flow — clean up and retry once, serially
  } else throw err;
}

Prevention

When it happens

Trigger: authenticate() -> waitForCallback() resolves with callbackParams whose `state` differs from the locally generated one — e.g. a stale tab from a previous run completing the callback on the reused localhost port, two CLI auth flows running concurrently on the same port, the provider dropping/rewriting the state param, or the callback URL being manually edited.

Common situations: An old browser tab from a previous failed auth attempt finally redirects and hits the new local server first; running two `connect` flows in parallel terminals where one server steals the other's callback; proxies or SSO intermediaries stripping query parameters; cookie/partitioned browser sessions mixing flows.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/afc976f268f332fd. Report an issue: GitHub.