Amblem
Furkan Baytekin

Implementing SSOT-Compatible JWT Authentication in NestJS with RSA Keys

Build a secure JWT auth system in NestJS with RSA keys and JWKS endpoint

Implementing SSOT-Compatible JWT Authentication in NestJS with RSA Keys
65
9 minutes

In microservice architectures, secure authentication is essential for maintaining trust between services. JSON Web Tokens (JWTs) are widely used for authentication, particularly in systems requiring a Single Source of Truth (SSOT). By leveraging RSA keys, you can create a secure, scalable, and SSOT-compatible authentication system where one service generates JWTs, and others verify them without creating new tokens. In this blog post, we’ll build such a system using NestJS, with an authentication backend generating JWTs using a private key, exposing a public key via a JWKS endpoint (/.well-known/jwks.json), and enabling other microservices to verify tokens.

This tutorial is aimed at developers familiar with Node.js, NestJS, and basic authentication concepts. By the end, you’ll have a working SSOT-compatible JWT authentication system.


Why Use RSA Keys for SSOT Authentication?

JWTs can be signed with symmetric keys (e.g., HMAC) or asymmetric keys (e.g., RSA). For an SSOT system in a microservices environment, RSA keys are ideal because:

Our goals are to:

  1. Build an authentication microservice that generates JWTs using a private RSA key.
  2. Expose a JWKS endpoint (/.well-known/jwks.json) to share the public key.
  3. Create a resource microservice that verifies JWTs using the public key, without generating new tokens.

Prerequisites

Before starting, ensure you have:


Step 1: Generating RSA Key Pairs

Generate an RSA key pair for signing and verifying JWTs. Run these commands in your terminal to create private and public keys in PEM format:

bash
# Generate a 2048-bit RSA private key openssl genrsa -out private.key 2048 # Extract the public key openssl rsa -in private.key -pubout -out public.key

Store these keys securely in a keys folder in your authentication service’s root directory. Do not commit these keys to version control.


Step 2: Setting Up the Authentication Microservice

The authentication microservice will generate JWTs using the private key and expose a JWKS endpoint for the public key.

2.1 Create a New NestJS Project

bash
nest new auth-service cd auth-service

2.2 Install Dependencies

Install the required packages for JWT handling and JWKS support:

bash
npm install @nestjs/jwt @nestjs/passport passport passport-jwt jwks-rsa node-jose npm install --save-dev @types/passport-jwt

2.3 Configure the Authentication Module

Create an auth module to manage JWT generation and the JWKS endpoint:

bash
nest g module auth nest g controller auth nest g service auth

In src/auth/auth.module.ts, configure the JwtModule with the private key:

typescript
import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import * as fs from 'fs'; import { join } from 'path'; @Module({ imports: [ JwtModule.register({ privateKey: fs.readFileSync(join(process.cwd(), 'keys/private.key')).toString(), signOptions: { expiresIn: '1h', algorithm: 'RS256' }, }), ], controllers: [AuthController], providers: [AuthService], }) export class AuthModule {}

2.4 Implement the Authentication Service

In src/auth/auth.service.ts, create a method to generate a JWT after validating user credentials (using a mock user for simplicity):

typescript
import { Injectable } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor(private jwtService: JwtService) {} async login(user: { username: string; userId: string }) { const payload = { sub: user.userId, username: user.username }; return { access_token: await this.jwtService.signAsync(payload, { algorithm: 'RS256', keyid: 'my-key-id', // Key ID for JWKS }), }; } // Mock user validation (replace with real database logic) async validateUser(username: string, password: string): Promise<any> { const user = { userId: '123', username: 'testuser' }; // Mock user if (username === 'testuser' && password === 'password') { return user; } return null; } }

2.5 Create the Login Endpoint

In src/auth/auth.controller.ts, add a login endpoint to authenticate users and return a JWT:

typescript
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { constructor(private authService: AuthService) {} @Post('login') async login(@Body() body: { username: string; password: string }) { const user = await this.authService.validateUser(body.username, body.password); if (!user) { throw new UnauthorizedException('Invalid credentials'); } return this.authService.login(user); } }

2.6 Expose the JWKS Endpoint

Create a new controller to serve the JWKS endpoint using node-jose:

bash
nest g controller jwks

In src/jwks/jwks.controller.ts, implement the JWKS endpoint:

typescript
import { Controller, Get } from '@nestjs/common'; import * as fs from 'fs'; import * as jose from 'node-jose'; import { join } from 'path'; @Controller('.well-known') export class JwksController { @Get('jwks.json') async getJwks() { const keyStore = jose.JWK.createKeyStore(); const publicKey = fs.readFileSync(join(process.cwd(), 'keys/public.key')).toString(); await keyStore.add(publicKey, 'pem', { alg: 'RS256', use: 'sig', kid: 'my-key-id' }); return keyStore.toJSON(); } }

Update src/auth/auth.module.ts to include the JwksController:

typescript
import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; import { AuthController } from './auth.controller'; import { AuthService } from './auth.service'; import { JwksController } from '../jwks/jwks.controller'; import * as fs from 'fs'; import { join } from 'path'; @Module({ imports: [ JwtModule.register({ privateKey: fs.readFileSync(join(process.cwd(), 'keys/private.key')).toString(), signOptions: { expiresIn: '1h', algorithm: 'RS256' }, }), ], controllers: [AuthController, JwksController], providers: [AuthService], }) export class AuthModule {}

The authentication service now provides a /auth/login endpoint to generate JWTs and a /.well-known/jwks.json endpoint to share the public key.


Step 3: Setting Up the Resource Microservice

The resource microservice will verify JWTs using the public key from the JWKS endpoint, ensuring it cannot generate new tokens.

3.1 Create a New NestJS Project

bash
nest new resource-service cd resource-service

3.2 Install Dependencies

bash
npm install @nestjs/passport passport passport-jwt jwks-rsa npm install --save-dev @types/passport-jwt

3.3 Configure the JWT Strategy

Create an auth module and implement a JwtStrategy:

bash
nest g module auth nest g service auth

In src/auth/jwt.strategy.ts, configure the JwtStrategy to fetch the public key dynamically:

typescript
import { Injectable } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { ExtractJwt, Strategy } from 'passport-jwt'; import { passportJwtSecret } from 'jwks-rsa'; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKeyProvider: passportJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: 'http://localhost:3000/.well-known/jwks.json', }), algorithms: ['RS256'], }); } async validate(payload: any) { return { userId: payload.sub, username: payload.username }; } }

3.4 Create an Authentication Guard

In src/auth/jwt-auth.guard.ts, create a guard to protect endpoints:

typescript
import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') {}

3.5 Configure the Auth Module

In src/auth/auth.module.ts, register the JwtStrategy:

typescript
import { Module } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { JwtStrategy } from './jwt.strategy'; @Module({ imports: [PassportModule.register({ defaultStrategy: 'jwt' })], providers: [JwtStrategy], }) export class AuthModule {}

3.6 Protect a Resource Endpoint

Create a resource module and a protected endpoint:

bash
nest g module resource nest g controller resource

In src/resource/resource.controller.ts, add a protected endpoint:

typescript
import { Controller, Get, UseGuards, Request } from '@nestjs/common'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; @Controller('resource') export class ResourceController { @UseGuards(JwtAuthGuard) @Get('protected') getProtectedResource(@Request() req) { return { message: 'This is a protected resource', user: req.user }; } }

Step 4: Testing the System

  1. Start the Authentication Service:
bash
cd auth-service npm run start
  1. Test the Login Endpoint: Use Postman or curl to send a POST request to http://localhost:3000/auth/login:
json
{ "username": "testuser", "password": "password" }

Response:

json
{ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im15LWtleS1pZCJ9..." }
  1. Verify the JWKS Endpoint: Visit http://localhost:3000/.well-known/jwks.json to ensure the public key is accessible.
  2. Start the Resource Service:
bash
cd resource-service npm run start
  1. Test the Protected Endpoint: Send a GET request to http://localhost:3001/resource/protected with the Authorization header:
text
Bearer <your_jwt_token>

If valid, you’ll see:

json
{ "message": "This is a protected resource", "user": { "userId": "123", "username": "testuser" } }

Step 5: Ensuring SSOT Compatibility

This system enforces SSOT principles because:

To enhance SSOT compatibility:


Best Practices


Conclusion

Using RSA keys and a JWKS endpoint, you can build an SSOT-compatible JWT authentication system in NestJS. The authentication service acts as the single source of truth, generating tokens with a private key, while resource services verify them using the public key from the JWKS endpoint. This approach ensures security, scalability, and adherence to SSOT principles.


Album of the day:

Suggested Blog Posts