{"record":{"id":"f346de3314660293","repo":"decolua/9router","slug":"token-exchange-failed-error-f346de","errorCode":null,"errorMessage":"Token exchange failed: ${error}","messagePattern":"Token exchange failed: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/lib/oauth/services/kiro.js","lineNumber":162,"sourceCode":"  async exchangeSocialCode(code, codeVerifier) {\n    // Must match the redirect_uri used in buildSocialLoginUrl\n    const redirectUri = \"kiro://kiro.kiroAgent/authenticate-success\";\n\n    const response = await fetch(`${KIRO_AUTH_SERVICE}/oauth/token`, {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application/json\",\n      },\n      body: JSON.stringify({\n        code,\n        code_verifier: codeVerifier,\n        redirect_uri: redirectUri,\n      }),\n    });\n\n    if (!response.ok) {\n      const error = await response.text();\n      throw new Error(`Token exchange failed: ${error}`);\n    }\n\n    const data = await response.json();\n    return {\n      accessToken: data.accessToken,\n      refreshToken: data.refreshToken,\n      profileArn: data.profileArn,\n      expiresIn: data.expiresIn || 3600,\n    };\n  }\n\n  /**\n   * Refresh token using refresh token\n   */\n  async refreshToken(refreshToken, providerSpecificData = {}) {\n    const { authMethod, clientId, clientSecret, region } = providerSpecificData;\n\n    // AWS SSO OIDC refresh (Builder ID or IDC)","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/src/lib/oauth/services/kiro.js#L144-L180","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Restart the social login flow (new buildSocialLoginUrl → new code + verifier) and complete the exchange immediately; authorization codes are short-lived and single-use.","Never modify the redirect_uri — it must stay kiro://kiro.kiroAgent/authenticate-success to match the authorization request.","Ensure the full code string from the callback is passed without truncation/URL-encoding damage.","Read the embedded upstream body for the exact OAuth error (invalid_grant, invalid_request, etc.)."],"exampleFix":"// before: retrying the same (already used) code\ntry { await svc.exchangeSocialCode(code, verifier); } catch { await svc.exchangeSocialCode(code, verifier); }\n// after: restart the flow to get a fresh code/verifier\nconst { url, codeChallenge, state } = newFlow();\nconst { code, verifier } = await completeLogin(url, state);\nawait svc.exchangeSocialCode(code, verifier);","handlingStrategy":"try-catch","validationCode":"function canExchange(code, verifier) {\n  return typeof code === 'string' && code.length > 10 &&\n         typeof verifier === 'string' && verifier.length >= 43; // PKCE S256 verifier min length\n}\nif (!canExchange(code, codeVerifier)) throw new Error('Missing or malformed code/verifier — restart the social login');","typeGuard":"function isTokenPair(d) { return typeof d?.accessToken === 'string' && typeof d?.refreshToken === 'string'; }","tryCatchPattern":"try {\n  return await svc.exchangeSocialCode(code, verifier);\n} catch (e) {\n  if (/invalid_grant|expired/i.test(e.message)) {\n    return startFreshSocialLogin(); // codes are single-use — never retry the same code\n  }\n  throw e;\n}","preventionTips":["Never retry an exchange with the same authorization code — it is single-use and short-lived.","Never alter redirect_uri; it must remain kiro://kiro.kiroAgent/authenticate-success exactly as in buildSocialLoginUrl.","Pass the code_verifier that generated the code_challenge for this specific state, not a stale one.","Complete the exchange immediately after the callback to stay inside the code's lifetime."],"tags":["oauth","pkce","social-login","token-exchange"],"backgroundTag":"oauth-token-exchange-failed","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}