Nest.js Blog Step by Step: Add Authorization
Wenhao Wang
Dev Intern · Leapcell

In the previous tutorial, we successfully built a fully functional personal blog. Anyone could visit and create new posts. However, for a true personal blog, typically only the blogger themselves can publish and manage content.
In this tutorial, we will add user authentication and authorization features to our blog. We will implement:
- User registration and login functionality.
- A Session-Cookie mechanism to manage user login state.
- Session persistence using Redis to ensure login state is not lost after a service restart.
- Protection for the post creation feature, allowing access only to logged-in users.
Without further ado, let's continue.
1. Choosing an Authentication Method
In web development, the two most common authentication methods are Token-based (e.g., JWT) and Session-based (Cookie).
- JWT (JSON Web Tokens): The server does not store user state. After a user logs in, the server generates an encrypted token and returns it to the client. The client includes this token in subsequent requests, and the server validates its legitimacy. This method is ideal for stateless RESTful APIs and Single Page Applications (SPAs).
- Session-Cookie: After a user logs in, the server creates a session and sends the Session ID back to the browser via a cookie. The browser automatically includes this cookie in subsequent requests. The server uses the Session ID from the cookie to find the corresponding session information, thereby identifying the user.
In this tutorial, we will choose the Session-Cookie method. The reason is that our blog is a traditional application with server-side rendered pages (EJS). In this model, the browser and server are tightly coupled, making cookie-based session management the most direct, classic, and secure method. Nest.js also provides excellent built-in support for this approach.
2. Creating the User Module
Similar to how we created the posts
module in the previous tutorial, we first need a module to manage users.
Use the Nest CLI to quickly generate the necessary files:
nest generate module users nest generate controller users nest generate service users
Next, create a user entity file user.entity.ts
in the src/users
directory to map to the users
table in the database.
// src/users/user.entity.ts import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; @Entity() export class User { @PrimaryGeneratedColumn('uuid') id: string; @Column({ unique: true }) username: string; @Column() password: string; // The stored password will be the encrypted hash }
Then, register the TypeOrmModule
in the UsersModule
(src/users/users.module.ts
) so it can operate on the User
entity.
// src/users/users.module.ts import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { User } from './user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], exports: [UsersService], // Export UsersService for use in other modules }) export class UsersModule {}
Finally, execute the following SQL statement in Leapcell or your local PostgreSQL database to create the user
table:
CREATE TABLE "user" ( "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), "username" VARCHAR UNIQUE NOT NULL, "password" VARCHAR NOT NULL );
3. Implementing Registration and Password Encryption
User passwords must never be stored in plaintext in the database. We will use the bcrypt
library to hash and encrypt passwords.
First, install the necessary dependencies:
npm install bcrypt npm install -D @types/bcrypt
Next, update src/users/users.service.ts
to add logic for creating and finding users.
// src/users/users.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from './user.entity'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User> ) {} async create(user: Partial<User>): Promise<User> { const saltRounds = 10; const hashedPassword = await bcrypt.hash(user.password || '', saltRounds); const newUser = this.usersRepository.create({ username: user.username, password: hashedPassword, }); return this.usersRepository.save(newUser); } async findOne(username: string): Promise<User | null> { return this.usersRepository.findOneBy({ username }); } }
4. Configuring Session and Passport.js
To handle login logic and session management, we will use passport
, a popular Node.js authentication library, along with express-session
to manage sessions.
Install the related dependencies:
npm install @nestjs/passport passport passport-local express-session npm install -D @types/passport-local @types/express-session
Create the Auth Module
We will place all authentication-related logic (like login, logout, strategies) into a separate auth
module.
nest generate module auth nest generate service auth
Modify src/auth/auth.service.ts
to add logic for validating user passwords.
// src/auth/auth.service.ts import { Injectable } from '@nestjs/common'; import { UsersService } from '../users/users.service'; import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { constructor(private usersService: UsersService) {} async validateUser(username: string, pass: string): Promise<any> { const user = await this.usersService.findOne(username); if (user && (await bcrypt.compare(pass, user.password))) { const { password, ...result } = user; return result; // Validation successful, return the user object without the password } return null; // Validation failed } }
Implement Passport Local Strategy
Passport uses "strategies" to handle different login methods. We will implement a "local strategy," which uses a username and password for login.
Create local.strategy.ts
in the src/auth
directory:
// src/auth/local.strategy.ts import { Strategy } from 'passport-local'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from './auth.service'; @Injectable() export class LocalStrategy extends PassportStrategy(Strategy) { constructor(private authService: AuthService) { super(); } async validate(username: string, password: string): Promise<any> { const user = await this.authService.validateUser(username, password); if (!user) { throw new UnauthorizedException(); } return user; } }
Configure the Auth Module
Now, let's assemble the service and strategy we just created into auth.module.ts
.
// src/auth/auth.module.ts import { Module } from '@nestjs/common'; import { AuthService } from './auth.service'; import { UsersModule } from '../users/users.module'; import { PassportModule } from '@nestjs/passport'; import { LocalStrategy } from './local.strategy'; @Module({ imports: [UsersModule, PassportModule], providers: [AuthService, LocalStrategy], }) export class AuthModule {}
5. Using Redis to Store Sessions
By default, express-session
stores sessions in the server's memory. This means that if the server restarts, all user login states will be lost. To solve this, we will use Redis, a high-performance in-memory database, to persist sessions.
What if you don't have Redis? If you don't have a Redis service available at the moment, you can skip this step.
express-session
will automatically fall back to using memory storage. This is perfectly fine for local development, but remember that it is not suitable for a production environment.
Install the Redis-related dependencies:
npm install redis connect-redis
Now, open the src/main.ts
file to configure the session middleware and connect to Redis.
// src/main.ts import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { join } from 'path'; import { AppModule } from './app.module'; import * as session from 'express-session'; import * as passport from 'passport'; import { createClient } from 'redis'; import RedisStore from 'connect-redis'; async function bootstrap() { const app = await NestFactory.create<NestExpressApplication>(AppModule); // Initialize Redis Client const redisClient = createClient({ // If your Redis has a password or is on a different host, modify the configuration here // url: 'redis://:password@hostname:port' url: 'redis://localhost:6379', }); await redisClient.connect().catch(console.error); // Initialize RedisStore const redisStore = new RedisStore({ client: redisClient, prefix: 'blog-session:', }); app.use( session({ store: redisStore, // Use Redis for storage secret: 'your-secret-key', // Replace with a random, complex string resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days }, }) ); app.use(passport.initialize()); app.use(passport.session()); // Passport Session Serialization and Deserialization // When login is successful, user information is serialized and stored in the session passport.serializeUser((user: any, done) => { done(null, user.id); }); // For each request, Passport will find the user via the ID in the session and deserialize it passport.deserializeUser(async (id: string, done) => { // Here you need a method to find a user by id, which we will add to UsersService later // const user = await usersService.findById(id); // For simplicity, we'll temporarily return an object with just the id done(null, { id }); }); app.useStaticAssets(join(__dirname, '..', 'public')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('ejs'); await app.listen(3000); } bootstrap();
Note: To make deserializeUser
work correctly, you will need to add a findById
method to your UsersService
.
6. Creating Login and Registration Pages
We need to provide an interface for users to interact with. In the views
folder, create login.ejs
and register.ejs
.
-
register.ejs
<%- include('_header', { title: 'Register' }) %> <form action="/users/register" method="POST" class="post-form"> <h2>Register</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Register</button> </form> <%- include('_footer') %>
-
login.ejs
<%- include('_header', { title: 'Login' }) %> <form action="/auth/login" method="POST" class="post-form"> <h2>Login</h2> <div class="form-group"> <label for="username">Username</label> <input type="text" id="username" name="username" required /> </div> <div class="form-group"> <label for="password">Password</label> <input type="password" id="password" name="password" required /> </div> <button type="submit">Login</button> </form> <%- include('_footer') %>
7. Implementing Routes and Controller Logic
Now, let's create the routes to handle registration and login requests.
Registration Route
Update src/users/users.controller.ts
:
// src/users/users.controller.ts import { Controller, Get, Post, Render, Body, Res } from '@nestjs/common'; import { UsersService } from './users.service'; import { Response } from 'express'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get('register') @Render('register') showRegisterForm() { return; } @Post('register') async register(@Body() body: any, @Res() res: Response) { // For simplicity, we won't do complex validation here await this.usersService.create(body); res.redirect('/auth/login'); // Redirect to the login page after successful registration } }
Login and Logout Routes
Create a new auth.controller.ts
to handle login and logout.
nest generate controller auth
Edit src/auth/auth.controller.ts
:
// src/auth/auth.controller.ts import { Controller, Get, Post, Render, Request, Res, UseGuards } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Response } from 'express'; @Controller('auth') export class AuthController { @Get('login') @Render('login') showLoginForm() { return; } @UseGuards(AuthGuard('local')) @Post('login') async login(@Request() req, @Res() res: Response) { // After successful validation, Passport attaches the user object to req.user res.redirect('/posts'); } @Get('logout') logout(@Request() req, @Res() res: Response) { req.logout((err) => { if (err) { console.log(err); } res.redirect('/'); }); } }
Finally, remember to import UsersModule
and AuthModule
in app.module.ts
.
// src/app.module.ts // ... imports import { UsersModule } from './users/users.module'; import { AuthModule } from './auth/auth.module'; @Module({ imports: [ // ... TypeOrmModule.forRoot(...) PostsModule, UsersModule, AuthModule, ], // ... }) export class AppModule {}
8. Protecting Routes and Updating the UI
Now that we have a login mechanism, the final step is to use it to protect our "create post" feature.
Create an Authentication Guard
A Guard is a class in Nest.js that determines whether a given request should be handled. We will create an AuthenticatedGuard
to check if a user is logged in.
Create authenticated.guard.ts
in the src/auth
directory:
// src/auth/authenticated.guard.ts import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; @Injectable() export class AuthenticatedGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest(); return request.isAuthenticated(); // Method added by Passport, checks if a user exists in the session } }
Apply the Guard
Open src/posts/posts.controller.ts
and apply the @UseGuards(AuthenticatedGuard)
decorator to the routes that need protection.
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Post, Body, Res, UseGuards } from '@nestjs/common'; import { AuthenticatedGuard } from 'src/auth/authenticated.guard'; // ...other imports @Controller('posts') export class PostsController { // ... constructor // ... findAll() @UseGuards(AuthenticatedGuard) // <--- Apply the guard @Get('new') @Render('new-post') newPostForm() { return; } @UseGuards(AuthenticatedGuard) // <--- Apply the guard @Post() async create(@Body() body: { title: string; content: string }, @Res() res: Response) { await this.postsService.create(body); res.redirect('/posts'); } // ... findOne() }
Now, if a non-logged-in user tries to access /posts/new
, they will be automatically intercepted.
Update the Frontend UI
Finally, let's update the UI to display different buttons based on the user's login status. We need to pass the user's login state to the frontend when rendering EJS templates. This requires a small modification to app.controller.ts
and posts.controller.ts
.
A simple way is to manually pass req.user
in every route that uses @Render
.
Modify _header.ejs
to add links for login/register and logout/new post.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><%= title %></title> <link rel="stylesheet" href="/css/style.css" /> </head> <body> <header> <h1><a href="/">My Blog</a></h1> <div class="user-actions"> <% if (user) { %> <span>Welcome, <%= user.username || 'User' %></span> <a href="/posts/new" class="new-post-btn">New Post</a> <a href="/auth/logout">Logout</a> <% } else { %> <a href="/auth/login">Login</a> <a href="/users/register">Register</a> <% } %> </div> </header> <main></main> </body> </html>
We need to update our controllers to pass the user
information to the views. In the root
method of posts.controller.ts
:
// src/posts/posts.controller.ts // ... import { Request } from 'express'; // Import Request @Controller('posts') export class PostsController { // ... @Get() @Render('index') async root(@Request() req) { // Inject the Request object const posts = await this.postsService.findAll(); return { posts, user: req.user }; // Pass the user object to the template } // ...make similar changes to other @Render routes @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); return { post, user: req.user }; } }
9. Running and Testing
Now, restart your application:
npm run start:dev
Visit http://localhost:3000
.
- You will see "Login" and "Register" buttons in the top-right corner.
- Click "Register" and create a new user.
- After successful registration, you will be redirected to the login page. Log in with the account you just created.
- After a successful login, you will see the buttons in the top-right corner change to "New Post" and "Logout". You can now create new posts.
At this point, we have successfully added a complete user authentication system to our blog.