decolua/9router · error

Token exchange failed: ${error}

Error message

Token exchange failed: ${error}

What it means

Thrown by KiroService.exchangeSocialCode when the Kiro auth service (https://prod.us-east-1.auth.desktop.kiro.dev/oauth/token) rejects the social-login authorization-code exchange with a non-2xx status; the upstream body is embedded in the message. The code+PKCE verifier could not be converted into access/refresh tokens, so Google/GitHub social login fails at the final step.

Source

Thrown at src/lib/oauth/services/kiro.js:162

  async exchangeSocialCode(code, codeVerifier) {
    // Must match the redirect_uri used in buildSocialLoginUrl
    const redirectUri = "kiro://kiro.kiroAgent/authenticate-success";

    const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/token`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        code,
        code_verifier: codeVerifier,
        redirect_uri: redirectUri,
      }),
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`Token exchange failed: ${error}`);
    }

    const data = await response.json();
    return {
      accessToken: data.accessToken,
      refreshToken: data.refreshToken,
      profileArn: data.profileArn,
      expiresIn: data.expiresIn || 3600,
    };
  }

  /**
   * Refresh token using refresh token
   */
  async refreshToken(refreshToken, providerSpecificData = {}) {
    const { authMethod, clientId, clientSecret, region } = providerSpecificData;

    // AWS SSO OIDC refresh (Builder ID or IDC)

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Restart the social login flow (new buildSocialLoginUrl → new code + verifier) and complete the exchange immediately; authorization codes are short-lived and single-use.
  2. Never modify the redirect_uri — it must stay kiro://kiro.kiroAgent/authenticate-success to match the authorization request.
  3. Ensure the full code string from the callback is passed without truncation/URL-encoding damage.
  4. Read the embedded upstream body for the exact OAuth error (invalid_grant, invalid_request, etc.).

Example fix

// before: retrying the same (already used) code
try { await svc.exchangeSocialCode(code, verifier); } catch { await svc.exchangeSocialCode(code, verifier); }
// after: restart the flow to get a fresh code/verifier
const { url, codeChallenge, state } = newFlow();
const { code, verifier } = await completeLogin(url, state);
await svc.exchangeSocialCode(code, verifier);
Defensive patterns

Strategy: try-catch

Validate before calling

function canExchange(code, verifier) {
  return typeof code === 'string' && code.length > 10 &&
         typeof verifier === 'string' && verifier.length >= 43; // PKCE S256 verifier min length
}
if (!canExchange(code, codeVerifier)) throw new Error('Missing or malformed code/verifier — restart the social login');

Type guard

function isTokenPair(d) { return typeof d?.accessToken === 'string' && typeof d?.refreshToken === 'string'; }

Try / catch

try {
  return await svc.exchangeSocialCode(code, verifier);
} catch (e) {
  if (/invalid_grant|expired/i.test(e.message)) {
    return startFreshSocialLogin(); // codes are single-use — never retry the same code
  }
  throw e;
}

Prevention

When it happens

Trigger: POST to /oauth/token with { code, code_verifier, redirect_uri } returns !response.ok — expired or already-consumed authorization code, wrong code_verifier (PKCE mismatch), or redirect_uri not matching the one used in buildSocialLoginUrl.

Common situations: User took too long between the browser login and the manual callback paste; the callback URL was edited or truncated so the code doesn't match the verifier; retrying with a one-time-use code; Cognito rejecting because redirect_uri differs from the whitelisted kiro:// URI.

Related errors


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