Securing the iCal Endpoint in PlayaMXCRM with a NestJS JWT Guard
TL;DR: Added a JWT guard to the iCal controller, updated the controller to use the guard, and removed obsolete API routes. This blocks unauthenticated requests to calendar data and centralizes auth logic in a reusable guard.
The Problem
The /ical endpoint in our API was exposed without any authentication checks. Anyone who knew the URL could hit it and retrieve calendar events, potentially exposing sensitive data. The error was obvious: no Authorization header validation, no rate limiting, and no audit trail. In production, this meant that internal calendar data could leak to external parties.
GET /ical?start=2026-07-01&end=2026-07-31
↳ 200 OK
↳ Returns full iCal feed
Without a guard, the controller looked like this before the change:
// apps/api/src/ical/ical.controller.ts
import { Controller, Get, Param, Query, Res } from "@nestjs/common";
import type { Response } from "express";
@Controller("ical")
export class IcalController {
@Get()
async getIcal(
@Query("start") start: string,
@Query("end") end: string,
@Res() res: Response,
) {
// ... fetch events, render iCal, send response
}
}
Notice the lack of any authentication or validation.
What I Tried First
My first attempt was to sprinkle a simple middleware across the route:
app.use("/ical", (req, res, next) => {
const auth = req.headers.authorization;
if (!auth) return res.status(401).send("Unauthorized");
// naive token check
next();
});
While it worked, it duplicated logic across controllers and made the codebase harder to maintain. I also tried adding a global guard, but that would affect all endpoints, not just /ical. I needed a fine‑grained solution.
The Implementation
1. Create a JWT Guard
I created a dedicated guard that decodes the JWT and attaches the payload to req.user. It uses the jsonwebtoken library and the shared secret.
// apps/api/src/common/guards/jwt.guard.ts
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { verify } from "jsonwebtoken";
@Injectable()
export class JwtGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return false;
}
const token = authHeader.split(" ")[1];
try {
const payload = verify(token, process.env.JWT_SECRET);
request.user = payload;
return true;
} catch (err) {
return false;
}
}
}
2. Apply the Guard to the iCal Controller
I updated the controller to import UseGuards and apply JwtGuard to the route. The controller now looks like this:
// apps/api/src/ical/ical.controller.ts
-import { Controller, Get, Param, Query, Res } from "@nestjs/common";
+import { Controller, Get, Param, Query, Res, UseGuards } from "@nestjs/common";
+import { JwtGuard } from "../common/guards/jwt.guard";
import type { Response } from "express";
@Controller("ical")
+@UseGuards(JwtGuard)
export class IcalController {
@Get()
async getIcal(
@Query("start") start: string,
@Query("end") end: string,
@Res() res: Response,
) {
// ... fetch events, render iCal, send response
}
}
Now, any request to /ical must include a valid JWT. The guard automatically rejects requests with a 403 status.
3. Clean Up the API Client
While refactoring the controller, I noticed that the api.ts file had a redundant print endpoint that was no longer used. Removing dead code improves maintainability and reduces surface area for attacks.
diff
// apps/web/src/lib/api.ts
@@
- print
---
*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.*
*Repo: `zaerohell/VS` · 2026-07-27*
\#playadev #buildinpublic
Top comments (0)