Building Secure Session-Based Authentication in NestJS – Part 1

Introduction

Session-based authentication is everywhere — yet many developers use it daily without fully understanding how it actually works.

In this series, we will build a real, production-style session-based authentication system using NestJS, Passport, Redis, and HTTP cookies. Along the way, we will demystify what happens on every request:

  • How sessions are created
  • Where they are stored
  • How they are validated
  • How session rotation works
  • How sessions are revoked

No magic. No hidden abstractions.

This article is Part 1 of a 4-part series. By the end of the series, we will have a complete, working application.

Prerequisites

Before starting, make sure you have the following installed:

PART 1 — Project Setup

Dependencies & Environment Variables

Project initialization

First, create a new NestJS project:

nest new nest-session

I will be using pnpm for this project, but you can use npm or yarn if you prefer.

Install the required dependencies:

pnpm i express-session redis connect-redis pg joi dotenv @nestjs/typeorm typeorm @nestjs/passport passport

And the development typings:

pnpm i -D @types/express-session @types/passport

Environment configuration

We will centralize and validate all environment variables using Joi.

Create the following file:

config/envs.ts

import 'dotenv/config';

import * as joi from 'joi';

interface EnvVars {
  PORT: number;
  NODE_ENV: string;

  GOOGLE_CLIENT_ID: string;
  GOOGLE_CLIENT_SECRET: string;
  GOOGLE_CALLBACK_URL: string;

  POSTGRES_HOST: string;
  POSTGRES_PORT: number;
  POSTGRES_USER: string;
  POSTGRES_PASSWORD: string;
  POSTGRES_DB_NAME: string;

  REDIS_PASSWORD: string;
  REDIS_HOST: string;
  REDIS_PORT: number;
  REDIS_URI: string;

  SESSION_SECRET: string;
}

const envsSchema = joi
  .object({
    PORT: joi.number().required(),
    NODE_ENV: joi.string().required(),

    GOOGLE_CLIENT_ID: joi.string().required(),
    GOOGLE_CLIENT_SECRET: joi.string().required(),
    GOOGLE_CALLBACK_URL: joi.string().required(),

    POSTGRES_HOST: joi.string().required(),
    POSTGRES_PORT: joi.number().required(),
    POSTGRES_USER: joi.string().required(),
    POSTGRES_PASSWORD: joi.string().required(),
    POSTGRES_DB_NAME: joi.string().required(),

    REDIS_PASSWORD: joi.string().required(),
    REDIS_HOST: joi.string().required(),
    REDIS_PORT: joi.number().required(),
    REDIS_URI: joi.string().required(),

    SESSION_SECRET: joi.string().required(),
  })
  .unknown(true);

const { error, value } = envsSchema.validate(process.env);

if (error) {
  throw new Error(`Config validation error: ${error.message}`);
}

const envVars: EnvVars = value;

export const envs = {
  PORT: envVars.PORT,
  NODE_ENV: envVars.NODE_ENV,

  GOOGLE_CLIENT_ID: envVars.GOOGLE_CLIENT_ID,
  GOOGLE_CLIENT_SECRET: envVars.GOOGLE_CLIENT_SECRET,
  GOOGLE_CALLBACK_URL: envVars.GOOGLE_CALLBACK_URL,

  POSTGRES_HOST: envVars.POSTGRES_HOST,
  POSTGRES_PORT: envVars.POSTGRES_PORT,
  POSTGRES_USER: envVars.POSTGRES_USER,
  POSTGRES_PASSWORD: envVars.POSTGRES_PASSWORD,
  POSTGRES_DB_NAME: envVars.POSTGRES_DB_NAME,

  REDIS_PASSWORD: envVars.REDIS_PASSWORD,
  REDIS_HOST: envVars.REDIS_HOST,
  REDIS_PORT: envVars.REDIS_PORT,
  REDIS_URI: envVars.REDIS_URI,

  SESSION_SECRET: envVars.SESSION_SECRET,
};

.env file

Make sure all required variables are defined in your .env file:

PORT=3000
NODE_ENV=development

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/callback

POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB_NAME=

REDIS_PASSWORD=
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_URI=redis://localhost:6379

SESSION_SECRET=

⚠️ Every variable must have a value, or the application will fail during startup.

OAuth provider (Google)

For this guide, we will use OAuth2 with Google as the authentication provider.

Create a new OAuth application and obtain your Client ID and Client Secret here:

https://console.cloud.google.com/

You can follow this series without OAuth by implementing traditional login and signup endpoints. The session concepts remain exactly the same.

Application entry point

Make sure your application uses the configured port:

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { envs } from './config/envs';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(envs.PORT ?? 3000);
}
bootstrap();

Auth & Redis Setup

Generate the Auth module

nest g res auth --no-spec

Select:

  • Transport layer: REST API
  • Generate CRUD endpoints: No

Configure the AppModule

app.module.ts

import { envs } from 'src/config/envs';
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [
    AuthModule,
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: envs.POSTGRES_HOST,
      port: envs.POSTGRES_PORT,
      username: envs.POSTGRES_USER,
      password: envs.POSTGRES_PASSWORD,
      database: envs.POSTGRES_DB_NAME,
      autoLoadEntities: true,
      synchronize: true, // ⚠️ Disable in production
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Redis Module

We will create a simple global Redis provider.

Create a new folder: src/redis

redis.constants.ts

export const REDIS_CLIENT = Symbol('REDIS_CLIENT');

redis.module.ts

import { Module } from '@nestjs/common';
import { createClient } from 'redis';
import { envs } from 'src/config/envs';
import { REDIS_CLIENT } from './redis.constants';

@Module({
  providers: [
    {
      provide: REDIS_CLIENT,
      useFactory: async () => {
        const client = createClient({
          url: envs.REDIS_URI,
          password: envs.REDIS_PASSWORD,
        });

        await client.connect();
        return client;
      },
    },
  ],
  exports: [REDIS_CLIENT],
})
export class RedisModule {}

User Entity

Before finalizing the auth setup, let’s define how our users will be stored.

auth/entities/user.entity.ts

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('user')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('varchar', { unique: true })
  email: string;

  @Column('varchar')
  fullName: string;

  @Column('varchar')
  provider: 'google' | 'github';

  @Column('varchar')
  providerId: string;

  @Column('varchar', { nullable: true })
  picture?: string;

  @Column('timestamp', { default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
}

AuthModule

auth.module.ts

import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { RedisModule } from 'src/redis/redis.module';

@Module({
  imports: [
    PassportModule.register({ session: true }),
    TypeOrmModule.forFeature([User]),
    RedisModule,
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Docker Setup

Create a docker-compose.yml file in the project root:

version: '3'

services:
  db:
    image: postgres:14.3
    restart: always
    ports:
      - '5432:5432'
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_DB: ${POSTGRES_DB_NAME}
    container_name: nest-session-postgres
    volumes:
      - ./postgres:/var/lib/postgresql/data

  redis:
    image: redis:7
    container_name: redis
    command: redis-server --requirepass "${REDIS_PASSWORD}"
    ports:
      - '${REDIS_PORT}:6379'
    volumes:
      - ./redis-data:/data
    restart: unless-stopped

Start the services:

docker compose up -d

Running the Application

Start the NestJS server:

pnpm run start:dev

At this point, your database schema should be created automatically.

You can verify it by connecting to Postgres using your favorite DB client (e.g. TablePlus) and confirming that the User table matches the entity definition.

user_table_correctly_loaded

In the next part, we’ll define our services, controllers, and guards, and explore how NestJS uses them to authenticate requests, manage session state, and protect routes.

Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Why Most “Free” Background Removers Aren’t Actually Free and the Best Alternative for 2025

Next Post

Monetzly: A Game Changer for AI Monetization in LLM Apps

Related Posts