{"record":{"id":"fc6e1ef1a553461e","repo":"calcom/cal.diy","slug":"invalid-access-token","errorCode":null,"errorMessage":"Invalid Access token.","messagePattern":"Invalid Access token\\.","errorType":"http","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"apps/api/v2/src/modules/conferencing/services/conferencing.service.ts","lineNumber":63,"sourceCode":"  async connectUserNonOauthApp(app: string, userId: number) {\n    switch (app) {\n      case GOOGLE_MEET:\n        const credential = await this.googleMeetService.connectGoogleMeetToUser(userId);\n        return credential;\n      default:\n        throw new BadRequestException(\"Invalid conferencing app. Available apps: GOOGLE_MEET.\");\n    }\n  }\n\n  async connectOauthApps(\n    app: string,\n    code: string,\n    decodedCallbackState: OAuthCallbackState,\n    teamId?: number\n  ) {\n    const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);\n    if (!userId) {\n      throw new UnauthorizedException(\"Invalid Access token.\");\n    }\n    switch (app) {\n      case ZOOM:\n        return await this.zoomVideoService.connectZoomApp(decodedCallbackState, code, userId, teamId);\n\n      case OFFICE_365_VIDEO:\n        return await this.office365VideoService.connectOffice365App(\n          decodedCallbackState,\n          code,\n          userId,\n          teamId\n        );\n\n      default:\n        throw new BadRequestException(\n          \"Invalid conferencing app, available apps are: \",\n          [ZOOM, OFFICE_365_VIDEO].join(\", \")\n        );","sourceCodeStart":45,"sourceCodeEnd":81,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/conferencing/services/conferencing.service.ts#L45-L81","documentation":"Thrown by ConferencingService.connectOauthApps when tokensRepository.getAccessTokenOwnerId returns null for the access token embedded in the decoded callback state. The token either does not exist in the DB, is expired, or is malformed. Returns HTTP 401 via UnauthorizedException. This is the user-binding step of the OAuth callback — it establishes which user the new credential belongs to.","triggerScenarios":"OAuth callback fires with a state.accessToken whose owner token was deleted (user logged out, token rotated); token expired during the OAuth round-trip; state was tampered with; user started the OAuth flow in one session and completed it in another (different accessToken).","commonSituations":"Long OAuth round-trip where the access token expired (default 30-day expiry); user cleared cookies mid-flow; access token revoked by an admin; dev/test token from a destroyed environment.","solutions":["Restart the OAuth flow with a fresh access token if the user's session expired mid-connect.","Verify the access token is still valid via a me/whoami call before generating the auth URL.","Lengthen token TTL or implement silent refresh so the OAuth round-trip survives.","Ensure the state is generated server-side from the current valid token, not a stale client-cached value."],"exampleFix":"// before: client caches token\nconst state = { accessToken: cachedToken, ... };\n\n// after: validate token freshness before starting the flow\nconst me = await api.me();\nif (!me) { await reauthenticate(); return; }\nconst { data } = await api.getOauthUrl(app); // server uses fresh token in state","handlingStrategy":"validation","validationCode":"async function accessTokenIsValid(tokensRepository: TokensRepository, token: string): Promise<boolean> {\n  const ownerId = await tokensRepository.getAccessTokenOwnerId(token);\n  return ownerId !== null;\n}\n\nconst state: OAuthCallbackState = JSON.parse(req.query.state);\nif (!(await accessTokenIsValid(tokensRepository, state.accessToken))) {\n  throw new UnauthorizedException('Session expired — please restart the OAuth flow.');\n}","typeGuard":"function hasFreshAccessToken(state: OAuthCallbackState & { accessTokenIssuedAt?: number }): boolean {\n  const maxAgeMs = 1000 * 60 * 60 * 24 * 30; // 30 days\n  return Boolean(state.accessToken) && (state.accessTokenIssuedAt ?? Date.now()) > Date.now() - maxAgeMs;\n}","tryCatchPattern":"try {\n  await conferencingService.connectOauthApps(app, code, decodedCallbackState, teamId);\n} catch (e) {\n  if (e instanceof UnauthorizedException && /Invalid Access token/.test(e.message)) {\n    // restart the flow with a fresh token\n    return res.redirect('/auth/login?next=/conferencing/connect');\n  }\n  throw e;\n}","preventionTips":["Generate the OAuth state from the current valid token at request time, not a cached one.","Validate the access token via a me() call before generating the auth URL.","Implement silent token refresh so the OAuth round-trip survives longer than the token TTL.","Track token issuance time and warn users before expiry."],"tags":["oauth","conferencing","access-token","auth","nestjs","session"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}