{"record":{"id":"1b20c0363dbef443","repo":"calcom/cal.diy","slug":"invalid-access-token-1b20c0","errorCode":null,"errorMessage":"Invalid Access token.","messagePattern":"Invalid Access token\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":401,"severity":"error","filePath":"apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts","lineNumber":96,"sourceCode":"        const responseBody = await result.json();\n        errorMessage = responseBody.error;\n      } catch (e) {\n        errorMessage = await result.clone().text();\n      }\n      throw new BadRequestException(errorMessage);\n    }\n\n    const responseBody = await result.json();\n\n    if (responseBody.error) {\n      throw new BadRequestException(responseBody.error);\n    }\n\n    responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);\n    delete responseBody.expires_in;\n\n    if (!userId) {\n      throw new UnauthorizedException(\"Invalid Access token.\");\n    }\n\n    const existingCredentialZoomVideo = teamId\n      ? await this.credentialsRepository.findAllCredentialsByTypeAndTeamId(ZOOM_TYPE, teamId)\n      : await this.credentialsRepository.findAllCredentialsByTypeAndUserId(ZOOM_TYPE, userId);\n\n    const credentialIdsToDelete = existingCredentialZoomVideo.map((item) => item.id);\n    if (credentialIdsToDelete.length > 0) {\n      teamId\n        ? await this.appsRepository.deleteTeamAppCredentials(credentialIdsToDelete, teamId)\n        : await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);\n    }\n\n    teamId\n      ? await this.appsRepository.createTeamAppCredential(\n          ZOOM_TYPE,\n          responseBody as unknown as Prisma.InputJsonObject,\n          teamId,","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts#L78-L114","documentation":"Thrown in connectZoomApp after a successful Zoom token exchange (status 200, no error) if userId is falsy. Because connectZoomApp signature requires userId: number, this guard fires when the caller passes 0, NaN coerced from undefined, or an explicitly falsy value. It raises UnauthorizedException (HTTP 401), distinct from a Zoom-side error.","triggerScenarios":"The controller/handler invoking connectZoomApp failed to resolve the authenticated user id and passed 0/undefined/NaN. The token exchange itself succeeded, but the service refuses to persist a credential with no owner.","commonSituations":"ApiAuthGuard did not attach request.user; the handler reads userId from a session that expired mid-flow; OAuth callback landed without the state param carrying userId; refactor changed the calling signature and a falsy default slipped in.","solutions":["Ensure the route is behind the API auth guard and that userId is extracted from request.user.id before calling connectZoomApp.","Validate userId is a positive integer at the controller boundary and return 401 there with a clearer message.","Check the OAuth state object is reconstructed correctly so the callback knows which user initiated the flow."],"exampleFix":"// before\nawait zoomService.connectZoomApp(state, code, req.user?.id ?? 0, teamId);\n\n// after\nconst userId = req.user?.id;\nif (!userId || !Number.isInteger(userId) || userId <= 0) {\n  throw new UnauthorizedException('Authenticated user required.');\n}\nawait zoomService.connectZoomApp(state, code, userId, teamId);","handlingStrategy":"validation","validationCode":"const userId = req.user?.id;\nif (!Number.isInteger(userId) || (userId as number) <= 0) {\n  throw new UnauthorizedException('Authenticated user required to connect Zoom.');\n}\nawait zoomService.connectZoomApp(state, code, userId as number, teamId);","typeGuard":"const isPositiveUserId = (u: unknown): u is number =>\n  typeof u === 'number' && Number.isInteger(u) && u > 0;","tryCatchPattern":null,"preventionTips":["Resolve and validate userId at the controller boundary, not deep in the service.","Ensure ApiAuthGuard populates request.user before the handler runs.","Encode userId in the OAuth state so callbacks can recover it."],"tags":["conferencing","zoom","auth","unauthorized","precondition","nestjs"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}