decolua/9router · error

Token refresh failed: ${error}

Error message

Token refresh failed: ${error}

What it means

Thrown by KiroService.refreshToken on the AWS SSO OIDC path (used when clientId and clientSecret are present, i.e. Builder ID / IDC device-flow accounts). The refresh call to oidc.<region>.amazonaws.com/token returned non-2xx and the AWS error body is included in the message. The stored refresh token could not be traded for a new access token.

Source

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

      assertValidAwsRegion(safeRegion);
      const endpoint = `https://oidc.${safeRegion}.amazonaws.com/token`;

      const response = await fetch(endpoint, {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          clientId,
          clientSecret,
          refreshToken,
          grantType: "refresh_token",
        }),
      });

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

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

    // Social auth refresh (Google/GitHub)
    const response = await fetch(`${KIRO_AUTH_SERVICE}/refreshToken`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Treat this as an expired/revoked session: re-run the full device-code login (registerClient → startDeviceAuthorization → pollDeviceToken) and store the new tokens.
  2. If the body shows expired client secret, call registerClient again for fresh clientId/clientSecret before refreshing.
  3. Check the embedded AWS error: invalid_grant means re-auth is required; slow_down/throttling means retry with backoff.
  4. Verify the persisted region matches the one the token was issued in.

Example fix

// before: refreshing forever with a dead token
try { await svc.refreshToken(rt, { clientId, clientSecret, region }); } catch { /* retry loop */ }
// after: re-authenticate on invalid_grant
try {
  await svc.refreshToken(rt, { clientId, clientSecret, region });
} catch (e) {
  if (/invalid_grant|InvalidRefreshToken/i.test(e.message)) return deviceCodeLogin();
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

function canAttemptOidcRefresh(psd) {
  return typeof psd?.clientId === 'string' && psd.clientId.length > 0 &&
         typeof psd?.clientSecret === 'string' && psd.clientSecret.length > 0 &&
         typeof psd?.refreshToken === 'string' && psd.refreshToken.startsWith('aorAAAAAG');
}
if (!canAttemptOidcRefresh(providerSpecificData)) throw new Error('Incomplete OIDC refresh credentials — re-authenticate');

Type guard

function isRefreshResult(r) { return typeof r?.accessToken === 'string' && r.accessToken.length > 0; }

Try / catch

try {
  return await svc.refreshToken(rt, { clientId, clientSecret, region });
} catch (e) {
  if (/invalid_grant|revoked|expired/i.test(e.message)) {
    markAccountNeedsReauth(accountId);
    return null; // caller shows re-login UI
  }
  if (/throttl|SlowDown/i.test(e.message)) return retryWithBackoff();
  throw e;
}

Prevention

When it happens

Trigger: POST to oidc.<region>.amazonaws.com/token with grantType refresh_token returns !response.ok — typically invalid_grant because the refresh token was revoked/expired, an expired clientSecret, or a wrong region.

Common situations: User revoked the device authorization in AWS Builder ID settings; token idle past AWS's refresh-token lifetime; clientSecretExpiresAt passed; account moved between regions; AWS throttling.

Related errors


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