Australian flagJoin us at the FIDO seminar in Melbourne – Feb 7, 2025!
nodejs express postgresql jwt authentication rolesAuthentication

Node.js Express JWT Authentication with PostgreSQL & Roles

This tutorial teaches you how to implement role-based JWT authentication in a Node.js backend using PostgreSQL

Blog-Post-Author

Lukas

Created: January 16, 2025

Updated: January 17, 2025


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:

  1. Comprehensive Flow for User Registration & Login with JWT Authentication
  2. Modern Node.js Express Architecture with CORS, Authentication & Authorization Middlewares, and Sequelize
  3. Configuration of Express Routes to Work Seamlessly with JWT
  4. Definition of Data Models and Associations for Robust Authentication and Authorization
  5. 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:

  • Header
  • Payload
  • Signature

These parts are concatenated into a standard structure: header.payload.signature.

Attaching JWT to Requests#

Clients typically include the JWT in the following headers:

  • Authorization Header with Bearer Prefix:

    Authorization: Bearer <token>
  • Custom Header:

    x-access-token: <token>

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#

MethodURLAction
POST/api/auth/signupRegister a new account
POST/api/auth/signinLog in to an account
GET/api/test/allRetrieve public content
GET/api/test/userAccess user content
GET/api/test/modAccess moderator content
GET/api/test/adminAccess admin content
Substack Icon

Subscribe to our Passkeys Substack for the latest news, insights and strategies.

Subscribe

3. Flow for Signup & Login with JWT Authentication#

  1. User Registration:
  • User submits signup details.
  • Server validates and creates a new user with a default role (user).
  1. User Login:
  • User submits login credentials.
  • Server verifies credentials and issues a JWT.
  1. 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
Demo Icon

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.
Slack Icon

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
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

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 };
Debugger Icon

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:

node server.js

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}.`); }); });
StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

16. Testing the API#

Use tools like Postman or Insomnia to test the API endpoints.

Register a New User#

  • Endpoint: POST /api/auth/signup

  • Body:

    { "username": "john_doe", "email": "john@example.com", "password": "securePassword123" }

Login#

  • Endpoint: POST /api/auth/signin

  • Body:

    { "username": "john_doe", "password": "securePassword123" }
  • Response:

    { "id": 1, "username": "john_doe", "email": "john@example.com", "roles": ["ROLE_ADMIN"], "accessToken": "your.jwt.token.here" }

Access Protected Routes#

Include the accessToken in the Authorization header as a Bearer token.

  • Header:

    Authorization: Bearer your.jwt.token.here
  • Endpoints:

    • GET /api/test/user - Accessible to authenticated users.
    • GET /api/test/mod - Accessible to moderators.
    • GET /api/test/admin - Accessible to admins.

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!

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.


We provide UI components, SDKs and guides to help you add passkeys to your app in <1 hour

Start for free