calcom/cal.diy · error · BadRequestException

Invalid Authorization Token.

Error message

Invalid Authorization Token.

What it means

Thrown by exchangeAuthorizationToken after the client matched, when oauthClient.authorizationTokens[0] is missing or has no owner.id. Authorization codes are single-use; once exchanged, invalidateAuthorizationToken marks them consumed. A second exchange of the same code, or a code that never had an owner, yields BadRequestException (HTTP 400).

Source

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

  async exchangeAuthorizationToken(
    tokenId: string,
    clientId: string,
    clientSecret: string
  ): Promise<KeysDto> {
    const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(
      tokenId,
      clientId,
      clientSecret
    );

    if (!oauthClient) {
      throw new BadRequestException("Invalid OAuth Client.");
    }

    const authorizationToken = oauthClient.authorizationTokens[0];

    if (!authorizationToken || !authorizationToken.owner.id) {
      throw new BadRequestException("Invalid Authorization Token.");
    }

    const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } =
      await this.tokensRepository.createOAuthTokens(clientId, authorizationToken.owner.id);
    await this.tokensRepository.invalidateAuthorizationToken(authorizationToken.id);
    void this.propagateAccessToken(accessToken); // void result, ignored.

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

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

View on GitHub (pinned to 176037d0af)

Solutions

  1. Treat the authorization code as single-use: cache the exchanged tokens and never re-exchange the same code.
  2. On a duplicate exchange, use the previously stored access/refresh tokens instead of re-running /exchange.
  3. Make the exchange step idempotent at the integration layer using a correlation key.
  4. If the code is genuinely fresh and still fails, re-authorize to get a new code.

Example fix

// before
await oauthFlow.exchangeAuthorizationToken(code, clientId, secret); // 400 on retry

// after — guard against re-exchange
let keys = tokenStore.get(code);
if (!keys) {
  keys = await oauthFlow.exchangeAuthorizationToken(code, clientId, secret);
  tokenStore.set(code, keys);
}
return keys;
Defensive patterns

Strategy: fallback

Validate before calling

// Never re-exchange the same authorization code
if (tokenStore.has(code)) return tokenStore.get(code);
const keys = await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);
tokenStore.set(code, keys);
return keys;

Type guard

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

Try / catch

try {
  return await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);
} catch (e) {
  if (e instanceof BadRequestException && /Authorization Token/.test(e.message)) {
    if (cachedKeys) return cachedKeys; // already exchanged previously
    const freshCode = await reAuthorize();
    return await oauthFlow.exchangeAuthorizationToken(freshCode, clientId, clientSecret);
  }
  throw e;
}

Prevention

When it happens

Trigger: Replaying an authorization code that was already exchanged; an authorization token that was invalidated; a token whose owning user was deleted (owner.id missing).

Common situations: Retry of /exchange after a network blip where the first call actually succeeded (code consumed); a duplicate webhook firing the exchange twice; a user re-running the OAuth dance and reusing the old code.

Related errors


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