calcom/cal.diy · error · UnauthorizedException

Invalid Access token.

Error message

Invalid Access token.

What it means

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.

Source

Thrown at apps/api/v2/src/modules/conferencing/services/conferencing.service.ts:63

  async connectUserNonOauthApp(app: string, userId: number) {
    switch (app) {
      case GOOGLE_MEET:
        const credential = await this.googleMeetService.connectGoogleMeetToUser(userId);
        return credential;
      default:
        throw new BadRequestException("Invalid conferencing app. Available apps: GOOGLE_MEET.");
    }
  }

  async connectOauthApps(
    app: string,
    code: string,
    decodedCallbackState: OAuthCallbackState,
    teamId?: number
  ) {
    const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
    if (!userId) {
      throw new UnauthorizedException("Invalid Access token.");
    }
    switch (app) {
      case ZOOM:
        return await this.zoomVideoService.connectZoomApp(decodedCallbackState, code, userId, teamId);

      case OFFICE_365_VIDEO:
        return await this.office365VideoService.connectOffice365App(
          decodedCallbackState,
          code,
          userId,
          teamId
        );

      default:
        throw new BadRequestException(
          "Invalid conferencing app, available apps are: ",
          [ZOOM, OFFICE_365_VIDEO].join(", ")
        );

View on GitHub (pinned to 176037d0af)

Solutions

  1. Restart the OAuth flow with a fresh access token if the user's session expired mid-connect.
  2. Verify the access token is still valid via a me/whoami call before generating the auth URL.
  3. Lengthen token TTL or implement silent refresh so the OAuth round-trip survives.
  4. Ensure the state is generated server-side from the current valid token, not a stale client-cached value.

Example fix

// before: client caches token
const state = { accessToken: cachedToken, ... };

// after: validate token freshness before starting the flow
const me = await api.me();
if (!me) { await reauthenticate(); return; }
const { data } = await api.getOauthUrl(app); // server uses fresh token in state
Defensive patterns

Strategy: validation

Validate before calling

async function accessTokenIsValid(tokensRepository: TokensRepository, token: string): Promise<boolean> {
  const ownerId = await tokensRepository.getAccessTokenOwnerId(token);
  return ownerId !== null;
}

const state: OAuthCallbackState = JSON.parse(req.query.state);
if (!(await accessTokenIsValid(tokensRepository, state.accessToken))) {
  throw new UnauthorizedException('Session expired — please restart the OAuth flow.');
}

Type guard

function hasFreshAccessToken(state: OAuthCallbackState & { accessTokenIssuedAt?: number }): boolean {
  const maxAgeMs = 1000 * 60 * 60 * 24 * 30; // 30 days
  return Boolean(state.accessToken) && (state.accessTokenIssuedAt ?? Date.now()) > Date.now() - maxAgeMs;
}

Try / catch

try {
  await conferencingService.connectOauthApps(app, code, decodedCallbackState, teamId);
} catch (e) {
  if (e instanceof UnauthorizedException && /Invalid Access token/.test(e.message)) {
    // restart the flow with a fresh token
    return res.redirect('/auth/login?next=/conferencing/connect');
  }
  throw e;
}

Prevention

When it happens

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

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

Related errors


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