{"record":{"id":"08bf78bc6bf02e4d","repo":"calcom/cal.diy","slug":"invalid-authorization-token","errorCode":null,"errorMessage":"Invalid Authorization Token.","messagePattern":"Invalid Authorization Token\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts","lineNumber":124,"sourceCode":"  async exchangeAuthorizationToken(\n    tokenId: string,\n    clientId: string,\n    clientSecret: string\n  ): Promise<KeysDto> {\n    const oauthClient = await this.oAuthClientRepository.getOAuthClientWithAuthTokens(\n      tokenId,\n      clientId,\n      clientSecret\n    );\n\n    if (!oauthClient) {\n      throw new BadRequestException(\"Invalid OAuth Client.\");\n    }\n\n    const authorizationToken = oauthClient.authorizationTokens[0];\n\n    if (!authorizationToken || !authorizationToken.owner.id) {\n      throw new BadRequestException(\"Invalid Authorization Token.\");\n    }\n\n    const { accessToken, refreshToken, accessTokenExpiresAt, refreshTokenExpiresAt } =\n      await this.tokensRepository.createOAuthTokens(clientId, authorizationToken.owner.id);\n    await this.tokensRepository.invalidateAuthorizationToken(authorizationToken.id);\n    void this.propagateAccessToken(accessToken); // void result, ignored.\n\n    return {\n      accessToken,\n      accessTokenExpiresAt: accessTokenExpiresAt.valueOf(),\n      refreshToken,\n      refreshTokenExpiresAt: refreshTokenExpiresAt.valueOf(),\n    };\n  }\n\n  async refreshToken(clientId: string, clientSecret: string, tokenSecret: string): Promise<KeysDto> {\n    const oauthClient = await this.oAuthClientRepository.getOAuthClientWithRefreshSecret(\n      clientId,","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/oauth-clients/services/oauth-flow.service.ts#L106-L142","documentation":"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).","triggerScenarios":"Replaying an authorization code that was already exchanged; an authorization token that was invalidated; a token whose owning user was deleted (owner.id missing).","commonSituations":"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.","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."],"exampleFix":"// before\nawait oauthFlow.exchangeAuthorizationToken(code, clientId, secret); // 400 on retry\n\n// after — guard against re-exchange\nlet keys = tokenStore.get(code);\nif (!keys) {\n  keys = await oauthFlow.exchangeAuthorizationToken(code, clientId, secret);\n  tokenStore.set(code, keys);\n}\nreturn keys;","handlingStrategy":"fallback","validationCode":"// Never re-exchange the same authorization code\nif (tokenStore.has(code)) return tokenStore.get(code);\nconst keys = await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);\ntokenStore.set(code, keys);\nreturn keys;","typeGuard":"function isExchangedKeys(value: unknown): value is { accessToken: string; refreshToken: string } {\n  return typeof value === 'object' && value !== null\n    && typeof (value as any).accessToken === 'string'\n    && typeof (value as any).refreshToken === 'string';\n}","tryCatchPattern":"try {\n  return await oauthFlow.exchangeAuthorizationToken(code, clientId, clientSecret);\n} catch (e) {\n  if (e instanceof BadRequestException && /Authorization Token/.test(e.message)) {\n    if (cachedKeys) return cachedKeys; // already exchanged previously\n    const freshCode = await reAuthorize();\n    return await oauthFlow.exchangeAuthorizationToken(freshCode, clientId, clientSecret);\n  }\n  throw e;\n}","preventionTips":["Treat authorization codes as single-use.","Cache the result keyed by code; never replay.","On a duplicate, reuse stored tokens, don't re-exchange."],"tags":["oauth","authorization-code","single-use","exchange","replay"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}