calcom/cal.diy · error · BadRequestException

Invalid refresh token

Error message

Invalid refresh token

What it means

Thrown by refreshToken after the client+secret matched, when oauthClient.refreshToken[0] is falsy. The refresh token row is absent — meaning the refresh token was revoked, deleted, already rotated (old one removed), or never existed for this client. BadRequestException (HTTP 400). The user must re-authorize to obtain a new grant.

Source

Thrown at apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts:154

      refreshTokenExpiresAt: refreshTokenExpiresAt.valueOf(),
    };
  }

  async refreshToken(clientId: string, clientSecret: string, tokenSecret: string): Promise<KeysDto> {
    const oauthClient = await this.oAuthClientRepository.getOAuthClientWithRefreshSecret(
      clientId,
      clientSecret,
      tokenSecret
    );

    if (!oauthClient) {
      throw new BadRequestException("Invalid OAuthClient credentials.");
    }

    const currentRefreshToken = oauthClient.refreshToken[0];

    if (!currentRefreshToken) {
      throw new BadRequestException("Invalid refresh token");
    }

    const { accessToken, refreshToken } = await this.tokensRepository.refreshOAuthTokens(
      clientId,
      currentRefreshToken.secret,
      currentRefreshToken.userId
    );

    return {
      accessToken: accessToken.secret,
      accessTokenExpiresAt: accessToken.expiresAt.valueOf(),
      refreshToken: refreshToken.secret,
      refreshTokenExpiresAt: refreshToken.expiresAt.valueOf(),
    };
  }

  private _generateActKey(accessToken: string) {
    return `act_${accessToken}`;

View on GitHub (pinned to 176037d0af)

Solutions

  1. Re-run the full OAuth authorize+exchange flow to obtain a fresh refresh token.
  2. If using refresh-token rotation, never reuse a refresh token that was already exchanged.
  3. Store only the latest refresh token after each refresh.
  4. Surface a 're-login required' prompt to the user when refresh fails this way.

Example fix

// before
await oauthFlow.refreshToken(clientId, secret, oldRefreshToken); // 400

// after — re-authorize to get a new grant
const code = await authorize(userId, clientId, redirectUri);
const { accessToken, refreshToken } = await oauthFlow.exchangeAuthorizationToken(code, clientId, secret);
await oauthFlow.refreshToken(clientId, secret, refreshToken);
Defensive patterns

Strategy: fallback

Validate before calling

// Track the latest refresh token; detect when re-authorize is required
function canAttemptRefresh(token: { refreshToken: string } | null | undefined): token is { refreshToken: string } {
  return Boolean(token && typeof token.refreshToken === 'string' && token.refreshToken.length > 0);
}
if (!canAttemptRefresh(stored)) {
  throw new Error('No usable refresh token; user must re-authorize');
}

Type guard

function hasRefreshToken(value: unknown): value is { refreshToken: string } {
  return typeof value === 'object' && value !== null && typeof (value as any).refreshToken === 'string';
}

Try / catch

try {
  return await oauthFlow.refreshToken(clientId, clientSecret, refreshToken);
} catch (e) {
  if (e instanceof BadRequestException && /refresh token/i.test(e.message)) {
    // refresh token revoked/rotated — fall back to full re-authorization
    const code = await reAuthorize(userId, clientId, redirectUri);
    return await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST /refresh with a refresh token that has been revoked or rotated out; a refresh token from a different client; a refresh token whose row was deleted.

Common situations: Refresh-token rotation policies that delete the old token; an admin revoked the session; the refresh token expired and was cleaned up; reusing a refresh token after it was already used under one-time-use rotation.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/7ce38156a06a9629. Report an issue: GitHub.