DEV Community

Roberto Luna
Roberto Luna

Posted on

Securing the iCal Endpoint in PlayaMXCRM with a NestJS JWT Guard

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
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
   }
 }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Top comments (0)