calcom/cal.diy · error · UnauthorizedException

Invalid Access token.

Error message

Invalid Access token.

What it means

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.

Solutions

  1. Ensure the route is behind the API auth guard and that userId is extracted from request.user.id before calling connectZoomApp.
  2. Validate userId is a positive integer at the controller boundary and return 401 there with a clearer message.
  3. Check the OAuth state object is reconstructed correctly so the callback knows which user initiated the flow.

Example fix

// before
await zoomService.connectZoomApp(state, code, req.user?.id ?? 0, teamId);

// after
const userId = req.user?.id;
if (!userId || !Number.isInteger(userId) || userId <= 0) {
  throw new UnauthorizedException('Authenticated user required.');
}
await zoomService.connectZoomApp(state, code, userId, teamId);
Defensive patterns

Strategy: validation

Validate before calling

const userId = req.user?.id;
if (!Number.isInteger(userId) || (userId as number) <= 0) {
  throw new UnauthorizedException('Authenticated user required to connect Zoom.');
}
await zoomService.connectZoomApp(state, code, userId as number, teamId);

Type guard

const isPositiveUserId = (u: unknown): u is number =>
  typeof u === 'number' && Number.isInteger(u) && u > 0;

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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

Appendix: source

Thrown at apps/api/v2/src/modules/conferencing/services/zoom-video.service.ts:96

        const responseBody = await result.json();
        errorMessage = responseBody.error;
      } catch (e) {
        errorMessage = await result.clone().text();
      }
      throw new BadRequestException(errorMessage);
    }

    const responseBody = await result.json();

    if (responseBody.error) {
      throw new BadRequestException(responseBody.error);
    }

    responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
    delete responseBody.expires_in;

    if (!userId) {
      throw new UnauthorizedException("Invalid Access token.");
    }

    const existingCredentialZoomVideo = teamId
      ? await this.credentialsRepository.findAllCredentialsByTypeAndTeamId(ZOOM_TYPE, teamId)
      : await this.credentialsRepository.findAllCredentialsByTypeAndUserId(ZOOM_TYPE, userId);

    const credentialIdsToDelete = existingCredentialZoomVideo.map((item) => item.id);
    if (credentialIdsToDelete.length > 0) {
      teamId
        ? await this.appsRepository.deleteTeamAppCredentials(credentialIdsToDelete, teamId)
        : await this.appsRepository.deleteAppCredentials(credentialIdsToDelete, userId);
    }

    teamId
      ? await this.appsRepository.createTeamAppCredential(
          ZOOM_TYPE,
          responseBody as unknown as Prisma.InputJsonObject,
          teamId,

View on GitHub (pinned to 176037d0af)