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.

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)

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.