calcom/cal.diy · critical · ForbiddenException

OAuthClientGuard - No OAuth client associated with the reque

Error message

OAuthClientGuard - No OAuth client associated with the request.

What it means

Thrown by the OAuthClientGuard (a NestJS CanActivate guard applied to OAuth-client-scoped routes) when request.params.clientId is falsy. The guard reads the client id from the route param; if the route was mounted without a :clientId segment, or the param is somehow absent, it rejects with ForbiddenException (HTTP 403). This is a routing/configuration defect, not a user input error.

Source

Thrown at apps/api/v2/src/modules/oauth-clients/guards/oauth-client-guard.ts:24

  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  NotFoundException,
} from "@nestjs/common";

@Injectable()
export class OAuthClientGuard implements CanActivate {
  constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest<ApiAuthGuardRequest>();
    const organizationId = this.getOrganizationId(context);
    const user: ApiAuthGuardUser = request.user;
    const oAuthClientId = request.params.clientId;

    if (!oAuthClientId) {
      throw new ForbiddenException("OAuthClientGuard - No OAuth client associated with the request.");
    }

    if (!user || !organizationId) {
      throw new ForbiddenException("OAuthClientGuard - No organization associated with the user.");
    }

    const oAuthClient = await this.oAuthClientRepository.getOAuthClient(oAuthClientId);

    if (!oAuthClient) {
      throw new NotFoundException("OAuthClientGuard - OAuth client not found.");
    }

    const allowed = Boolean(user.isSystemAdmin || oAuthClient.organizationId === organizationId);
    if (!allowed) {
      throw new ForbiddenException(
        `OAuthClientGuard - forbidden. oAuth client with id=${oAuthClientId} does not belong to the organization with id=${organizationId}.`
      );
    }

View on GitHub (pinned to 176037d0af)

Solutions

  1. Ensure every route guarded by OAuthClientGuard includes :clientId in its path, e.g. @Controller(':clientId') at the class level or :clientId in each @Post/@Get path.
  2. In unit tests, mock request.params = { clientId: '<id>' } before invoking the guard.
  3. Verify the controller's @Controller() prefix and method path together contain :clientId.
  4. Grep the module routing to confirm no guard-protected route lacks the param.

Example fix

// before
@Controller('oauth-clients')
@UseGuards(OAuthClientGuard)
export class OAuthClientController {
  @Get('events')           // no :clientId — guard throws
  getEvents() { ... }
}

// after
@Controller('oauth-clients/:clientId')
@UseGuards(OAuthClientGuard)
export class OAuthClientController {
  @Get('events')           // :clientId now in scope
  getEvents() { ... }
}
Defensive patterns

Strategy: validation

Validate before calling

// Server-side: assert the route exposes :clientId before mounting the guard
function assertRouteHasClientId(path: string) {
  if (!/:clientId(\b|\?|$|\()/.test(path)) {
    throw new Error(`Route '${path}' uses OAuthClientGuard but has no :clientId param`);
  }
}

Type guard

function requestHasClientId(req: { params?: Record<string, unknown> }): req is { params: { clientId: string } } {
  return typeof req.params?.clientId === 'string' && (req.params as any).clientId.length > 0;
}

Try / catch

// Guard-level: throw early and descriptive — callers cannot recover from a routing bug.
// Fix the route definition rather than catching at runtime.

Prevention

When it happens

Trigger: A controller method decorated with @UseGuards(OAuthClientGuard) is mounted on a route that has no :clientId parameter, or the request reaches it via a path that didn't populate the param. Reproducible only by misconfigured routing — a correctly mounted route always has the param.

Common situations: Refactoring a controller and forgetting to keep :clientId in the route path; mounting the controller under a prefix that already consumed the param; a test that invokes the handler directly without a mocked params object.

Related errors


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