---
title: "Implementing SSOT-Compatible JWT Authentication in NestJS with RSA Keys"
description: "Learn how to implement secure JWT authentication in NestJS using RSA keys and a JWKS endpoint while maintaining Single Source of Truth principles."
slug: "implementing-ssot-compatible-jwt-authentication-in-nestjs-with-rsa-keys"
url: "https://furkanbaytekin.dev/blogs/implementing-ssot-compatible-jwt-authentication-in-nestjs-with-rsa-keys"
author: "Furkan Baytekin"
category: "Software"
tags: []
published: 2025-05-30T08:53:13Z
updated: 2025-05-30T08:53:13Z
readTime: 9
cover: "https://myblogcms.furkanbaytekin.dev/uploads/cover/implementing-ssot-compatible-jwt-authentication-in-nestjs-with-rsa-keys+1748122904689"
---

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:

- **Security**: The private key remains with the authentication service, while the public key is shared for verification, minimizing key exposure risks.
- **Single Source of Truth**: Only the authentication service generates JWTs, ensuring centralized control over token issuance.
- **Interoperability**: A JWKS (JSON Web Key Set) endpoint aligns with standards like OpenID Connect, facilitating integration with other systems.

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:

- **Node.js** (v16 or higher) and **NestJS CLI** installed (`npm i -g @nestjs/cli`).
- **OpenSSL** to generate RSA key pairs.
- Basic knowledge of NestJS, JWT, and microservices.

---

## 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
```

2. **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..."
}
```

3. **Verify the JWKS Endpoint**: Visit `http://localhost:3000/.well-known/jwks.json` to ensure the public key is accessible.
4. **Start the Resource Service**:

```bash
cd resource-service
npm run start
```

5. **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:

- **Centralized Token Issuance**: Only the authentication service generates JWTs using the private key.
- **Public Key Distribution**: The JWKS endpoint allows secure, standardized access to the public key.
- **Verification Only**: Resource services can only verify tokens, not create them, maintaining the authentication service as the SSOT.

To enhance SSOT compatibility:

- Implement **refresh tokens** for session management.
- Add **token revocation** for security (e.g., blacklisting).
- Integrate with an identity provider for production-grade SSOT.

---

## Best Practices

- **Secure Key Storage**: Use a secrets manager (e.g., AWS Secrets Manager) instead of storing keys in the filesystem.
- **Environment Variables**: Configure sensitive data like the JWKS URI using a `.env` file.
- **Rate Limiting**: Limit JWKS endpoint requests to prevent abuse.
- **Token Expiry**: Use short-lived tokens (e.g., 1 hour) with refresh tokens.
- **Validation**: Add `issuer` and `audience` checks in the `JwtStrategy` for enhanced security.

---

## 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:

<iframe style="border-radius:12px" src="https://open.spotify.com/embed/album/19ttKxyRQWA1cZmmwfoDPS?utm_source=generator" width="100%" height="152" frameBorder="0" allowfullscreen="" allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>
