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

  1. 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.
  2. On the client side, detect 'already authorized' and skip straight to /exchange using the existing authorization code.
  3. In tests, use a fresh user or clean up authorization tokens between authorize calls.
  4. 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

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


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