DEV Community

Cover image for Secure Service-to-Service Auth: Verifying Laravel JWTs in a NestJS Microservice
Noni Gopal Sutradhar Rinku
Noni Gopal Sutradhar Rinku

Posted on

Secure Service-to-Service Auth: Verifying Laravel JWTs in a NestJS Microservice

In a microservices world, your Laravel application often handles user authentication (via Sanctum or Passport) and issues a JWT (JSON Web Token). The challenge is securely allowing a separate, downstream service (like a NestJS data processor) to verify that token without needing to access Laravel's database on every single request.

The solution is to use the JWT verification public key, a pattern known as JWKS (JSON Web Key Set).

1. The Core Security Principle

Instead of the NestJS service asking Laravel: "Is this token valid?", the NestJS service asks: "Does the cryptographic signature of this token match the public key provided by Laravel?"

Laravel (Issuer): Uses its private key to sign the JWT, ensuring integrity.

NestJS (Verifier): Uses Laravel's public key (usually exposed via a public endpoint) to cryptographically verify the token's signature.

This process is fast, stateless, and secure.

2. Laravel Setup (The Issuer)

If you are using Laravel Passport, it already exposes the necessary public key. If you are using Sanctum, you may need a small package to expose the JWKS endpoint, but the principle is the same: the public key must be accessible.

The key file is typically found at: storage/oauth-public.key.

3. NestJS Setup (The Verifier)

We will use NestJS's standard @nestjs/passport and @nestjs/jwt strategy. The key is configuring the JwtModule to use the public key for verification.

A. The JWT Verification Strategy

We define a simple Passport Strategy to pull the token from the header and verify it using the public key.

// src/auth/jwt.strategy.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import * as fs from 'fs';
import * as path from 'path';

// Load the public key from the Laravel API environment
// NOTE: In production, load this key via an environment variable or KMS.
const pathToPublicKey = path.join(__dirname, '..', '..', 'config', 'laravel_public.key');
const publicKey = fs.readFileSync(pathToPublicKey, 'utf8');

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false, // Always respect token expiration
      secretOrKey: publicKey,  // <-- CRITICAL: Use the Laravel Public Key here
      algorithms: ['RS256'],   // Use the signing algorithm Laravel Passport uses
    });
  }

  // The payload contains the user information embedded by Laravel
  async validate(payload: any) {
    if (!payload.user_id) {
        throw new UnauthorizedException('Token payload is missing user ID.');
    }
    // You can now access the user ID via req.user.user_id in your controllers
    return payload; 
  }
}
Enter fullscreen mode Exit fullscreen mode

B. The Auth Module

We create an AuthModule to register the strategy and make it available application-wide.

// src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';

@Module({
  imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
  providers: [JwtStrategy],
  exports: [PassportModule],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

4. Final Step: Securing Controllers

In any NestJS controller that requires a valid, verified token issued by your Laravel API, simply apply the AuthGuard.

// src/user-data/user-data.controller.ts

import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('user-data')
export class UserDataController {

  // Use the default 'jwt' strategy defined in AuthModule
  @UseGuards(AuthGuard('jwt')) 
  @Get('profile-status')
  getProfileStatus(@Req() req) {
    // If execution reaches here, the token signature was verified by the public key.
    const userId = req.user.user_id; // Access the user ID from the validated payload

    return { status: `Data for user ${userId}` };
  }
}
Enter fullscreen mode Exit fullscreen mode

Let me know your suggestions about this post.

Top comments (0)