{"record":{"id":"bc00dfb9199b8217","repo":"calcom/cal.diy","slug":"user-with-id-userid-has-already-authorized-clie","errorCode":null,"errorMessage":"User with id=${userId} has already authorized client with id=${clientId}.","messagePattern":"User with id=(.+?) has already authorized client with id=(.+?)\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"warning","filePath":"apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts","lineNumber":73,"sourceCode":"    @GetUser(\"id\") userId: number,\n    @Response() res: ExpressResponse\n  ): Promise<void> {\n    const oauthClient = await this.oauthClientRepository.getOAuthClient(clientId);\n    if (!oauthClient) {\n      throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);\n    }\n\n    if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {\n      throw new BadRequestException(\"Invalid 'redirect_uri' value.\");\n    }\n\n    const alreadyAuthorized = await this.tokensRepository.getAuthorizationTokenByClientUserIds(\n      clientId,\n      userId\n    );\n\n    if (alreadyAuthorized) {\n      throw new BadRequestException(\n        `User with id=${userId} has already authorized client with id=${clientId}.`\n      );\n    }\n\n    const { id } = await this.tokensRepository.createAuthorizationToken(clientId, userId);\n\n    return res.redirect(`${body.redirectUri}?code=${id}`);\n  }\n\n  @Post(\"/exchange\")\n  @HttpCode(HttpStatus.OK)\n  @DocsExcludeEndpoint()\n  async exchange(\n    @Headers(\"Authorization\") authorization: string,\n    @Param(\"clientId\") clientId: string,\n    @Body() body: ExchangeAuthorizationCodeInput\n  ): Promise<KeysResponseDto> {\n    const authorizeEndpointCode = authorization.replace(\"Bearer \", \"\").trim();","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/oauth-clients/controllers/oauth-flow/oauth-flow.controller.ts#L55-L91","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait oauthFlow.authorize(clientId, { redirectUri }, userId); // throws if already authorized\n\n// after — check first, reuse existing\nconst existing = await tokensRepository.getAuthorizationTokenByClientUserIds(clientId, userId);\nif (existing) {\n  return res.redirect(`${redirectUri}?code=${existing.id}`);\n}\nawait oauthFlow.authorize(clientId, { redirectUri }, userId);","handlingStrategy":"validation","validationCode":"// Check whether an authorization token already exists before calling authorize\nconst existing = await tokensRepository.getAuthorizationTokenByClientUserIds(clientId, userId);\nif (existing) {\n  return res.redirect(`${redirectUri}?code=${existing.id}`);\n}\nawait oauthFlow.authorize(clientId, { redirectUri }, userId);","typeGuard":"function hasExistingAuthorization(t: unknown): t is { id: string } {\n  return typeof t === 'object' && t !== null && typeof (t as any).id === 'string';\n}","tryCatchPattern":"try {\n  await oauthFlow.authorize(clientId, { redirectUri }, userId);\n} catch (e) {\n  if (e instanceof BadRequestException && /already authorized/i.test(e.message)) {\n    // surface 'already connected' state to the UI; proceed to /exchange\n  } else throw e;\n}","preventionTips":["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."],"tags":["oauth","authorization","duplicate","idempotency","state"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}