This tutorial teaches you how to implement role-based JWT authentication in a Node.js backend using PostgreSQL
In this tutorial, we'll build a modern Node.js Express REST API that supports token-based authentication using JWT (JSON Web Token) and PostgreSQL. By the end of this guide, you'll understand:
Comprehensive Flow for User Registration & Login with JWT Authentication
Modern Node.js Express Architecture with CORS, Authentication & Authorization Middlewares, and Sequelize
Configuration of Express Routes to Work Seamlessly with JWT
Definition of Data Models and Associations for Robust Authentication and Authorization
Utilization of Sequelize for Efficient Interaction with PostgreSQL Database
Let's dive in!
1. Token-Based Authentication#
Unlike session-based authentication, which stores session data on the server and relies on cookies, token-based authentication stores the JWT on the client side—such as in Local Storage for browsers, Keychain for iOS, or SharedPreferences for Android. This approach eliminates the need for an additional backend to support native apps or separate authentication modules.
A JWT consists of three parts:
These parts are concatenated into a standard structure: header.payload.signature
.
Attaching JWT to Requests#
Clients typically include the JWT in the following headers:
2. Overview of Node.js JWT Authentication with PostgreSQL#
We'll build a Node.js Express application where users can:
Sign Up: Create a new account.
Sign In: Log in with a username and password.
Access Resources: Access public and role-protected resources based on their assigned roles (admin
, moderator
, user
).
Required APIs#
Method URL Action POST /api/auth/signup
Register a new account POST /api/auth/signin
Log in to an account GET /api/test/all
Retrieve public content GET /api/test/user
Access user content GET /api/test/mod
Access moderator content GET /api/test/admin
Access admin content
Subscribe to our Passkeys Substack for the latest news, insights and strategies.
Subscribe
3. Flow for Signup & Login with JWT Authentication#
User Registration:
User submits signup details.
Server validates and creates a new user with a default role (user
).
User Login:
User submits login credentials.
Server verifies credentials and issues a JWT.
Authorization:
User includes JWT in requests to access protected resources.
Server verifies JWT and authorizes access based on user roles.
Security Note: Ensure that the JWT is securely stored on the client side to prevent unauthorized access.
4. Modern Node.js Express Architecture with Authentication & Authorization#
Our architecture comprises:
Express Routes: Handle incoming HTTP requests.
CORS Middleware: Manage Cross-Origin Resource Sharing.
Security Layer:
JWT Authentication Middleware: Verifies JWTs during signup and token verification.
Authorization Middleware: Checks user roles against database records.
Controllers: Interact with the PostgreSQL database via Sequelize and respond to client requests with relevant data or tokens.
5. Technology Stack#
Node.js: JavaScript runtime.
Express: Web framework for Node.js.
Tailwind CSS: Modern CSS framework (replacing Bootstrap).
Sequelize: Promise-based ORM for Node.js.
PostgreSQL: Relational database system.
jsonwebtoken: For JWT creation and verification.
bcryptjs: For hashing passwords.
6. Project Structure#
node-js-jwt-auth-postgresql/
├── app/
│ ├── config/
│ │ ├── auth.config.js
│ │ └── db.config.js
│ ├── controllers/
│ │ ├── auth.controller.js
│ │ └── user.controller.js
│ ├── middlewares/
│ │ ├── authJwt.js
│ │ └── verifySignUp.js
│ ├── models/
│ │ ├── index.js
│ │ ├── role.model.js
│ │ └── user.model.js
│ └── routes/
│ ├── auth.routes.js
│ └── user.routes.js
├── server.js
├── package.json
└── tailwind.config.js
Want to try passkeys yourself? Check our Passkeys Demo.
Try Passkeys
7. Setting Up the Node.js Application#
Initialize the Project#
Create a new directory and initialize a Node.js project:
mkdir node-js-jwt-auth-postgresql
cd node-js-jwt-auth-postgresql
npm init -y
Install Dependencies#
Install the necessary packages:
npm install express sequelize pg pg-hstore cors jsonwebtoken bcryptjs
To enable ES Module syntax, update package.json
:
{
"type" : "module" ,
// ... other configurations
}
8. Setting Up the Express Web Server#
Create a server.js
file in the root directory:
// server.js
import express from 'express' ;
import cors from 'cors' ;
import db from './app/models/index.js' ;
import authRoutes from './app/routes/auth.routes.js' ;
import userRoutes from './app/routes/user.routes.js' ;
const app = express ();
const corsOptions = {
origin: 'http://localhost:8081'
};
app. use ( cors (corsOptions));
// Parse requests of content-type - application/json
app. use (express. json ());
// Parse requests of content-type - application/x-www-form-urlencoded
app. use (express. urlencoded ({ extended: true }));
// Simple route
app. get ( '/' , ( req , res ) => {
res. json ({ message: 'Welcome to the Node.js JWT Authentication application.' });
});
// Routes
app. use ( '/api/auth' , authRoutes);
app. use ( '/api/test' , userRoutes);
// Set port, listen for requests
const PORT = process.env. PORT || 8080 ;
db.sequelize. sync (). then (() => {
app. listen ( PORT , () => {
console. log ( `Server is running on port ${ PORT }.` );
});
});
Explanation:
CORS Configuration: Allows requests from http://localhost:8081
.
Middleware: Parses JSON and URL-encoded data.
Routes: Includes authentication and user-related routes.
Database Synchronization: Ensures Sequelize syncs models with the PostgreSQL database before starting the server.
Become part of our Passkeys Community for updates and support.
Join
9. Configuring PostgreSQL Database & Sequelize#
Create a configuration file for the database in app/config/db.config.js
:
// app/config/db.config.js
export default {
HOST: 'localhost' ,
USER: 'postgres' ,
PASSWORD: 'your_password' ,
DB: 'testdb' ,
dialect: 'postgres' ,
pool: {
max: 5 ,
min: 0 ,
acquire: 30000 ,
idle: 10000
}
} ;
Initializing Sequelize#
Set up Sequelize in app/models/index.js
:
// app/models/index.js
import Sequelize from "sequelize" ;
import dbConfig from "../config/db.config.js" ;
import UserModel from "./user.model.js" ;
import RoleModel from "./role.model.js" ;
const sequelize = new Sequelize (dbConfig. DB , dbConfig. USER , dbConfig. PASSWORD , {
host: dbConfig. HOST ,
dialect: dbConfig.dialect,
pool: dbConfig.pool
});
const db = {};
db.Sequelize = Sequelize;
db.sequelize = sequelize;
db.user = UserModel (sequelize, Sequelize);
db.role = RoleModel (sequelize, Sequelize);
db.role. belongsToMany (db.user, { through: "user_roles" });
db.user. belongsToMany (db.role, { through: "user_roles" , as: "roles" });
db. ROLES = [ "user" , "admin" , "moderator" ];
export default db;
Explanation:
Models: Imports User
and Role
models.
Associations: Establishes a Many-to-Many relationship between users
and roles
through the user_roles
table.
Roles Array: Defines available roles.
Setting up a local PostgresSQL Database#
If you do not have a local PostgreSQL instance available for testing already, you can set up a temporary local
database using Docker:
docker run --rm -e POSTGRES_PASSWORD=your_password -e POSTGRES_USER=postgres -e POSTGRES_DB=testdb -p 5432:5432 postgres
10. Defining the Sequelize Models#
User Model#
Create app/models/user.model.js
:
// app/models/user.model.js
export default (sequelize, DataTypes) => {
const User = sequelize. define ( 'users' , {
username: {
type: DataTypes. STRING ,
unique: true
},
email: {
type: DataTypes. STRING ,
unique: true ,
validate: {
isEmail: true
}
},
password: {
type: DataTypes. STRING
}
});
return User;
};
Role Model#
Create app/models/role.model.js
:
// app/models/role.model.js
export default (sequelize, DataTypes) => {
const Role = sequelize. define ( 'roles' , {
id: {
type: DataTypes. INTEGER ,
primaryKey: true ,
autoIncrement: true
},
name: {
type: DataTypes. STRING
}
});
return Role;
};
Security Enhancement: By making username
and email
unique and validating email formats, we enhance data integrity and security.
11. Configuring Authentication Key#
Create app/config/auth.config.js
to store your secret key:
// app/config/auth.config.js
export default {
secret: 'your-very-secure-secret-key'
} ;
Security Note: Use a strong, unpredictable secret key and store it securely, preferably using environment variables.
12. Creating Middleware Functions#
Verify Sign-Up Middleware#
Create app/middlewares/verifySignUp.js
:
// app/middlewares/verifySignUp.js
import db from '../models/index.js' ;
const ROLES = db. ROLES ;
const User = db.user;
const checkDuplicateUsernameOrEmail = async ( req , res , next ) => {
try {
// Check if username exists
let user = await User. findOne ({ where: { username: req.body.username } });
if (user) {
return res. status ( 400 ). json ({ message: 'Failed! Username is already in use!' });
}
// Check if email exists
user = await User. findOne ({ where: { email: req.body.email } });
if (user) {
return res. status ( 400 ). json ({ message: 'Failed! Email is already in use!' });
}
next ();
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
const checkRolesExisted = ( req , res , next ) => {
if (req.body.roles) {
for ( const role of req.body.roles) {
if ( ! ROLES . includes (role)) {
return res. status ( 400 ). json ({ message: `Failed! Role does not exist: ${ role }` });
}
}
}
next ();
};
const verifySignUp = {
checkDuplicateUsernameOrEmail,
checkRolesExisted
};
export default verifySignUp;
JWT Authentication Middleware#
Create app/middlewares/authJwt.js
:
// app/middlewares/authJwt.js
import jwt from 'jsonwebtoken' ;
import db from '../models/index.js' ;
import authConfig from '../config/auth.config.js' ;
const User = db.user;
const verifyToken = ( req , res , next ) => {
const token = req.headers[ 'x-access-token' ] || req.headers[ 'authorization' ];
if ( ! token) {
return res. status ( 403 ). json ({ message: 'No token provided!' });
}
const actualToken = token. startsWith ( 'Bearer ' ) ? token. slice ( 7 , token. length ) : token;
jwt. verify (actualToken, authConfig.secret, ( err , decoded ) => {
if (err) {
return res. status ( 401 ). json ({ message: 'Unauthorized!' });
}
req.userId = decoded.id;
next ();
});
};
const isAdmin = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
for ( const role of roles) {
if (role.name === 'admin' ) {
return next ();
}
}
return res. status ( 403 ). json ({ message: 'Require Admin Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
const isModerator = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
for ( const role of roles) {
if (role.name === 'moderator' ) {
return next ();
}
}
return res. status ( 403 ). json ({ message: 'Require Moderator Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
const isModeratorOrAdmin = async ( req , res , next ) => {
try {
const user = await User. findByPk (req.userId);
const roles = await user. getRoles ();
for ( const role of roles) {
if (role.name === 'moderator' || role.name === 'admin' ) {
return next ();
}
}
return res. status ( 403 ). json ({ message: 'Require Moderator or Admin Role!' });
} catch (error) {
res. status ( 500 ). json ({ message: error.message });
}
};
const authJwt = {
verifyToken,
isAdmin,
isModerator,
isModeratorOrAdmin
};
export default authJwt;
Security Enhancements:
Token Verification: Properly verifies JWTs and handles Bearer tokens.
Role Checks: Asynchronously verifies user roles to prevent blocking the event loop.
Error Handling: Returns appropriate HTTP status codes and error messages.
Middleware Index#
Create app/middlewares/index.js
:
// app/middlewares/index.js
import authJwt from './authJwt.js' ;
import verifySignUp from './verifySignUp.js' ;
export { authJwt, verifySignUp };
Want to experiment with passkey flows? Try our Passkeys Debugger.
Try for Free
13. Creating Controllers#
Authentication Controller#
Create app/controllers/auth.controller.js
:
// app/controllers/auth.controller.js
import db from '../models/index.js' ;
import authConfig from '../config/auth.config.js' ;
import jwt from 'jsonwebtoken' ;
import bcrypt from 'bcryptjs' ;
const User = db.user;
const Role = db.role;
export const signup = async ( req , res ) => {
try {
// Create new user
const hashedPassword = await bcrypt. hash (req.body.password, 10 );
const user = await User. create ({
username: req.body.username,
email: req.body.email,
password: hashedPassword
});
const role = await Role. findOne ({where: {name: 'user' }});
await user. setRoles ([role]);
res. status ( 201 ). json ({message: 'User registered successfully!' });
} catch (error) {
res. status ( 500 ). json ({message: error.message});
}
};
export const signin = async ( req , res ) => {
try {
// Find user by username
const user = await User. findOne ({
where: {
username: req.body.username
}
});
if ( ! user) {
return res. status ( 404 ). json ({message: 'User Not found.' });
}
// Validate password
const passwordIsValid = await bcrypt. compare (req.body.password, user.password);
if ( ! passwordIsValid) {
return res. status ( 401 ). json ({
accessToken: null ,
message: 'Invalid Password!'
});
}
// Generate JWT
const token = jwt. sign ({id: user.id}, authConfig.secret, {
expiresIn: 86400 // 24 hours
});
// Get user roles
const roles = await user. getRoles ();
const authorities = roles. map ( role => `ROLE_${ role . name . toUpperCase () }` );
res. status ( 200 ). json ({
id: user.id,
username: user.username,
email: user.email,
roles: authorities,
accessToken: token
});
} catch (error) {
res. status ( 500 ). json ({message: error.message});
}
};
User Controller#
Create app/controllers/user.controller.js
:
// app/controllers/user.controller.js
export const allAccess = ( req , res ) => {
res. status ( 200 ). send ( 'Public Content.' );
};
export const userBoard = ( req , res ) => {
res. status ( 200 ). send ( 'User Content.' );
};
export const moderatorBoard = ( req , res ) => {
res. status ( 200 ). send ( 'Moderator Content.' );
};
export const adminBoard = ( req , res ) => {
res. status ( 200 ). send ( 'Admin Content.' );
};
14. Defining Routes#
Authentication Routes#
Create app/routes/auth.routes.js
:
// app/routes/auth.routes.js
import express from 'express' ;
import { signup, signin } from '../controllers/auth.controller.js' ;
import { verifySignUp } from '../middlewares/index.js' ;
const router = express. Router ();
// Signup Route
router. post (
'/signup' ,
[
verifySignUp.checkDuplicateUsernameOrEmail,
verifySignUp.checkRolesExisted
],
signup
);
// Signin Route
router. post ( '/signin' , signin);
export default router;
User Routes#
Create app/routes/user.routes.js
:
// app/routes/user.routes.js
import express from 'express' ;
import { allAccess, userBoard, moderatorBoard, adminBoard } from '../controllers/user.controller.js' ;
import { authJwt } from '../middlewares/index.js' ;
const router = express. Router ();
// Public Route
router. get ( '/all' , allAccess);
// User Route
router. get ( '/user' , [authJwt.verifyToken], userBoard);
// Moderator Route
router. get ( '/mod' , [authJwt.verifyToken, authJwt.isModerator], moderatorBoard);
// Admin Route
router. get ( '/admin' , [authJwt.verifyToken, authJwt.isAdmin], adminBoard);
export default router;
15. Running the Application#
Starting the Server#
Ensure PostgreSQL is running and the database testdb
is created. Then, start the server:
You should see:
Server is running on port 8080.
Initializing Roles#
Implement role initialization in server.js
to populate the roles
table:
// server.js (add below the import statements)
const initializeRoles = async () => {
const roles = [ 'user' , 'moderator' , 'admin' ];
for ( const role of roles) {
await db.role. findOrCreate ({
where: { name: role }
});
}
};
db.sequelize. sync (). then ( async () => {
await initializeRoles ();
app. listen ( PORT , () => {
console. log ( `Server is running on port ${ PORT }.` );
});
});
16. Testing the API#
Use tools like Postman or Insomnia to test the API endpoints.
Register a New User#
Login#
Access Protected Routes#
Include the accessToken
in the Authorization
header as a Bearer token.
17. Conclusion#
Congratulations! You've successfully built a secure and modern Node.js Express REST API with JWT authentication and PostgreSQL using Sequelize. This setup provides a solid foundation for user authentication and role-based authorization, ensuring that your application adheres to current best practices.
Next Steps:
Implement Refresh Tokens: Enhance security by implementing refresh tokens to allow users to obtain new access tokens without re-authenticating.
Integrate Front-End: Develop a front-end application using frameworks like React, Angular, or Vue.js to interact with your API.
Deploy to Production: Prepare your application for deployment by setting up environment variables, secure HTTPS connections, and scalable infrastructures.
Happy coding!