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
- Treat the authorization code as single-use: cache the exchanged tokens and never re-exchange the same code.
- On a duplicate exchange, use the previously stored access/refresh tokens instead of re-running /exchange.
- Make the exchange step idempotent at the integration layer using a correlation key.
- 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
- Treat authorization codes as single-use.
- Cache the result keyed by code; never replay.
- On a duplicate, reuse stored tokens, don't re-exchange.
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
- Missing 'Bearer' Authorization header.
- PermissionsGuard - no authentication provided. Provide eithe
- PermissionsGuard - oAuth client with id=${oAuthClient.id} do
- PermissionsGuard - no oAuth client found for access token=${
- PermissionsGuard - no oAuth client found for client id=${id}
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/08bf78bc6bf02e4d.
Report an issue: GitHub.