calcom/cal.diy · warning · BadRequestException
User with id=${userId} has already authorized client with id
Error message
User with id=${userId} has already authorized client with id=${clientId}. What it means
Thrown by POST /authorize when tokensRepository.getAuthorizationTokenByClientUserIds(clientId, userId) returns a truthy value, meaning an authorization token already exists for this user+client pair. The OAuth flow does not silently re-authorize — it rejects the duplicate with BadRequestException (HTTP 400). This is by design: each user authorizes a given client once; subsequent flows must use the existing grant or revoke it first.
Source
Thrown at apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts:73
@GetUser("id") userId: number,
@Response() res: ExpressResponse
): Promise<void> {
const oauthClient = await this.oauthClientRepository.getOAuthClient(clientId);
if (!oauthClient) {
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
}
if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
throw new BadRequestException("Invalid 'redirect_uri' value.");
}
const alreadyAuthorized = await this.tokensRepository.getAuthorizationTokenByClientUserIds(
clientId,
userId
);
if (alreadyAuthorized) {
throw new BadRequestException(
`User with id=${userId} has already authorized client with id=${clientId}.`
);
}
const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId);
return res.redirect(`${body.redirectUri}?code=${id}`);
}
@Post("/exchange")
@HttpCode(HttpStatus.OK)
@DocsExcludeEndpoint()
async exchange(
@Headers("Authorization") authorization: string,
@Param("clientId") clientId: string,
@Body() body: ExchangeAuthorizationCodeInput
): Promise<KeysResponseDto> {
const authorizeEndpointCode = authorization.replace("Bearer ", "").trim();View on GitHub (pinned to 176037d0af)
Solutions
- If the user intends to re-authorize, revoke/invalidate the existing authorization token for (clientId, userId) first via the tokens repository or a revoke endpoint.
- On the client side, detect 'already authorized' and skip straight to /exchange using the existing authorization code.
- In tests, use a fresh user or clean up authorization tokens between authorize calls.
- Make the authorize button idempotent: check connection status before showing it.
Example fix
// before
await oauthFlow.authorize(clientId, { redirectUri }, userId); // throws if already authorized
// after — check first, reuse existing
const existing = await tokensRepository.getAuthorizationTokenByClientUserIds(clientId, userId);
if (existing) {
return res.redirect(`${redirectUri}?code=${existing.id}`);
}
await oauthFlow.authorize(clientId, { redirectUri }, userId); Defensive patterns
Strategy: validation
Validate before calling
// Check whether an authorization token already exists before calling authorize
const existing = await tokensRepository.getAuthorizationTokenByClientUserIds(clientId, userId);
if (existing) {
return res.redirect(`${redirectUri}?code=${existing.id}`);
}
await oauthFlow.authorize(clientId, { redirectUri }, userId); Type guard
function hasExistingAuthorization(t: unknown): t is { id: string } {
return typeof t === 'object' && t !== null && typeof (t as any).id === 'string';
} Try / catch
try {
await oauthFlow.authorize(clientId, { redirectUri }, userId);
} catch (e) {
if (e instanceof BadRequestException && /already authorized/i.test(e.message)) {
// surface 'already connected' state to the UI; proceed to /exchange
} else throw e;
} Prevention
- Make the authorize action idempotent: check connection status first.
- In tests, clean up authorization tokens between runs.
- Disable the authorize button once a connection exists.
When it happens
Trigger: A user who previously clicked 'Authorize' for the same client clicks authorize again without the prior authorization token having been revoked or exchanged-and-invalidated. Re-running an OAuth integration setup, or a retry of an authorize call after a partial success.
Common situations: Re-running onboarding for an integration that was already connected; double-click on an authorize button; a developer testing the authorize endpoint repeatedly against the same user without cleaning up tokens.
Related errors
- PermissionsGuard - oAuth client with id=${oAuthClient.id} do
- Missing `state` query param
- Ooo entry already exists.
- Webhook with this subscriber url already exists for this oAu
- Can't cancel booking with uid=${bookingUid} because it has b
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/bc00dfb9199b8217.
Report an issue: GitHub.