calcom/cal.diy · error · ForbiddenException

PermissionsGuard - no oAuth client found for access token=${

Error message

PermissionsGuard - no oAuth client found for access token=${accessToken}

What it means

ForbiddenException from PermissionsGuard.getOAuthClientByAccessToken when tokensRepository.getAccessTokenClient(accessToken) returns null — i.e. the supplied Bearer access token does not match any row in the platform access-tokens store. The token itself is echoed back in the message (note: this leaks the credential into logs/responses — a hygiene concern).

Source

Thrown at apps/api/v2/src/modules/auth/guards/permissions/permissions.guard.ts:79

        `PermissionsGuard - oAuth client with id=${
          oAuthClient.id
        } does not have the required permissions=${requiredPermissions
          .map((permission) => this.oAuthClientsOutputService.transformOAuthClientPermission(permission))
          .join(
            ", "
          )}. Go to platform dashboard settings and add the required permissions to the oAuth client.`
      );
    }

    return true;
  }

  async getOAuthClientByAccessToken(
    accessToken: string
  ): Promise<Pick<PlatformOAuthClient, "id" | "permissions">> {
    const oAuthClient = await this.tokensRepository.getAccessTokenClient(accessToken);
    if (!oAuthClient) {
      throw new ForbiddenException(
        `PermissionsGuard - no oAuth client found for access token=${accessToken}`
      );
    }
    return oAuthClient;
  }

  async getOAuthClientById(id: string): Promise<Pick<PlatformOAuthClient, "id" | "permissions">> {
    const oAuthClient = await this.oAuthClientRepository.getOAuthClient(id);
    if (!oAuthClient) {
      throw new ForbiddenException(`PermissionsGuard - no oAuth client found for client id=${id}`);
    }
    return oAuthClient;
  }

  getDecodedThirdPartyAccessToken(bearerToken: string) {
    return this.tokensService.getDecodedThirdPartyAccessToken(bearerToken);
  }
}

View on GitHub (pinned to 176037d0af)

Solutions

  1. Refresh the access token using the OAuth2 refresh_token grant, then retry.
  2. Confirm the token was issued by this platform's OAuth server and not by a third-party IdP.
  3. If revoked, re-issue via the authorize flow.
  4. As a platform maintainer: do NOT echo the raw accessToken back in the error message — log only a truncated/hash identifier to avoid credential leakage.

Example fix

// before — leaks the credential
throw new ForbiddenException(
  `PermissionsGuard - no oAuth client found for access token=${accessToken}`
);

// after — redact
throw new ForbiddenException(
  `PermissionsGuard - no oAuth client found for access token=${accessToken.slice(0, 6)}…`
);
Defensive patterns

Strategy: retry

Validate before calling

// Decode JWT exp client-side to catch expiry before sending
function isLikelyExpired(token: string): boolean {
  try {
    const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
    return typeof payload.exp === 'number' && payload.exp * 1000 < Date.now();
  } catch { return true; }
}

Try / catch

try {
  await client.get('/v2/protected');
} catch (e) {
  if (e.status === 403 && /no oAuth client found for access token/.test(e.message)) {
    await refreshToken(); // refresh_token grant, then retry once
    return client.get('/v2/protected');
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling a PermissionsGuard-protected endpoint with a Bearer token that is expired, revoked, malformed, or issued by a different system (e.g. a NextAuth session token that wasn't short-circuited, or a third-party token that getDecodedThirdPartyAccessToken didn't recognize).

Common situations: Token expired (access tokens are short-lived); token revoked from the dashboard; copy/paste truncation; using a refresh token where an access token is required; environment drift between issuing and verifying API; clock skew causing premature expiry.

Related errors


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