nodejs express postgresql jwt authentication roles

Node.js Express PostgreSQL Authentication with JWT & Roles

Learn how to build a secure Node.js Express backend with PostgreSQL authentication, JWT authorization, and role-based access control in this step-by-step guide.

Blog-Post-Author

Lukas

Created: January 16, 2025

Updated: February 17, 2025


Our mission is to make the Internet a safer place, and the new login standard passkeys provides a superior solution to achieve that. That's why we want to help you understand passkeys and its characteristics better.

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#

1.1 How does token-based authentication work?#

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.

1.2 Attaching JWT to requests#

A JWT consists of three parts:

  • Header
  • Payload
  • Signature

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

Clients typically include the JWT in the following headers:

  • Authorization Header with Bearer Prefix:

    Authorization: Bearer <token>
  • Custom Header:

    x-access-token: <token>

2. Why use PostgreSQL for authentication in Node.js Express?#

PostgreSQL is a robust and scalable relational database that integrates seamlessly with Node.js and Express for building secure and efficient backend systems. It offers several benefits for implementing authentication and authorization:

  • Advanced Data Management: PostgreSQL supports relational data models, making it easier to manage user roles, permissions, and relationships.
  • Security Features: Built-in tools like row-level security and encryption ensure sensitive data, such as user credentials, remains secure.
  • JSON Support: PostgreSQL's ability to handle JSON data simplifies the integration of token-based authentication mechanisms like JWT.
  • High Scalability: With its capability to handle concurrent requests, PostgreSQL is ideal for applications with a growing user base.

By leveraging PostgreSQL, you can ensure a scalable and secure solution for role-based authentication in your Node.js backend.

3. 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

4. How JWT secures Node.js REST APIs with PostgreSQL#

JWT (JSON Web Token) is a lightweight, stateless authentication method ideal for REST APIs. Here's how it integrates with PostgreSQL:

  1. Token Structure: A JWT contains three parts: the header (algorithm and token type), the payload (user information and claims), and the signature (signed with a secret key).
  2. Verification: The server verifies the JWT using the secret key and extracts the user ID. Middleware handles this process:
jwt.verify(token, authConfig.secret, (err, decoded) => { if (err) return res.status(401).json({ message: 'Unauthorized!' }); req.userId = decoded.id; next(); });
  1. Role-based access: PostgreSQL stores user roles, which are checked after JWT verification to enforce access restrictions.

This setup ensures that only authorized users can access protected resources.

Substack Icon

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

Subscribe

5. Flow for Signup & Login with JWT Authentication#

jwt auth sign up sign in

6.1 User Registration#

  • User submits signup details.
  • Server validates and creates a new user with a default role (user).

6.2 User Login#

  • User submits login credentials.
  • Server verifies credentials and issues a JWT.

6.3 Authorization#

  • User includes JWT in requests to access protected resources.
  • Server verifies JWT and authorizes access based on user roles.

Role-based authorization in Node.js Express using PostgreSQL:

Role-based authorization allows applications to enforce access control by assigning specific permissions to users. Here’s how it works in a Node.js Express backend:

  1. Defining roles: PostgreSQL is used to define roles like admin, moderator, and user. These roles are stored in the roles table and linked to users through a many-to-many relationship. Example Role Definition:
db.role.belongsToMany(db.user, { through: "user_roles" }); db.user.belongsToMany(db.role, { through: "user_roles" });
  1. Assigning roles during signup: When a new user registers, a default role (e.g., user) is assigned. This ensures that every user has a role from the start. Example Code:
const role = await Role.findOne({ where: { name: 'user' } }); await user.setRoles([role]);
  1. Authorization middleware: Middleware is used to check user roles during requests. For example:
const isAdmin = async (req, res, next) => { const user = await User.findByPk(req.userId); const roles = await user.getRoles(); if (roles.some(role => role.name === 'admin')) { return next(); } return res.status(403).json({ message: 'Require Admin Role!' }); };

This structure ensures fine-grained access control, making it easier to secure sensitive endpoints.

Security Note: Ensure that the JWT is securely stored on the client side to prevent unauthorized access.

You also need to refresh tokens when they expire:

refresh token

7. Top benefits of using JWT for Node.js Express authentication#

JWT provides several benefits when securing Node.js Express applications:

  • Stateless Authentication: Unlike session-based systems, JWT does not require server-side storage, making it more scalable.
  • Cross-Platform Compatibility: JWT works seamlessly with web, mobile, and desktop applications.
  • Enhanced Security: Tokens can be signed and encrypted to prevent tampering.
  • Integration with PostgreSQL: The database can efficiently verify user roles and permissions based on the JWT payload.

8. Seamless role management in Node.js REST APIs#

Managing roles is critical for secure and efficient access control. PostgreSQL simplifies this with its robust data management capabilities:

  1. Role assignment during user creation: When a user signs up, they are automatically assigned a default role, such as user.
const role = await Role.findOne({ where: { name: 'user' } }); await user.setRoles([role]);
  1. Dynamic role updates: Admins can modify user roles dynamically via API endpoints:
const updateRoles = async (userId, newRoles) => { const user = await User.findByPk(userId); const roles = await Role.findAll({ where: { name: newRoles } }); await user.setRoles(roles); };
  1. Role-based middleware: Middleware checks roles before granting access to specific endpoints. For example:
const isModerator = async (req, res, next) => { const roles = await req.user.getRoles(); if (roles.some(role => role.name === 'moderator')) { return next(); } return res.status(403).json({ message: 'Require Moderator Role!' }); };

9. Modern Node.js Express Architecture with Authentication & Authorization#

react nodejs express postgresql architecture

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.

10. 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.

11. 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

12. Setting Up the Node.js Application#

12.1 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

12.2 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 }

13. 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

14. 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 } };

14.1 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.

14.2 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

15. Defining the Sequelize Models#

15.1 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; };

15.2 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.

16. 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.

17. Creating Middleware Functions#

17.1 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;

17.2 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

18. Creating Controllers#

18.1 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}); } };

18.2 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.'); };

19. Defining Routes#

19.1 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;

19.2 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;

20 Running the Application#

20.1 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.

20.2 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

21. Testing the API#

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

21.1 Register a New User#

  • Endpoint: POST /api/auth/signup

  • Body:

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

21.2 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" }

21.3 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.

24 Common mistakes to avoid when implementing Node.js JWT authentication#

Avoid these common pitfalls to ensure secure and effective JWT implementation:

  1. Weak secret keys: Use a strong, unpredictable secret key for signing JWTs. Store it securely using environment variables.
  2. Insecure storage: Do not store JWTs in local storage for web apps. Use HTTP-only cookies for better security.
  3. Ignoring expiry: Set a reasonable expiration time for tokens and implement refresh tokens for longer sessions.
  4. Poor error handling: Ensure detailed error messages are logged but do not reveal sensitive information to the client.

25. 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!

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

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.