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

Node.js Express JWT Authentication with MySQL & Roles

This tutorial teaches you how to build a secure Node.js Express REST API application with JWT Authentication based on roles and MySQL.

Blog-Post-Author

Lukas

Created: January 15, 2025

Updated: January 17, 2025


1. Introduction#

In this tutorial, we'll build a modern Node.js Express REST API that supports Token-Based Authentication using JWT (JSON Web Tokens). You'll learn how to:

  • Implement user signup and login flows with JWT authentication.
  • Structure a Node.js Express application with CORS, authentication & authorization middlewares, and Sequelize ORM.
  • Configure Express routes to work seamlessly with JWT.
  • Define data models and associations for robust authentication and authorization.
  • Utilize Sequelize to interact with a MySQL database.

By the end of this guide, you'll have a secure and scalable backend ready to support your applications.

2. Understanding Token-Based Authentication#

Token-based authentication offers significant advantages over traditional session-based methods. Instead of storing session information on the server or in cookies, JWTs are stored client-side—such as in Local Storage for web browsers, Keychain for iOS, or SharedPreferences for Android. This approach eliminates the need for additional backend projects or authentication modules for native app users.

JWT Structure#

A JWT consists of three parts:

  1. Header
  2. Payload
  3. Signature

These parts are concatenated as header.payload.signature. Typically, the client attaches the JWT in the Authorization header with the Bearer prefix:

Authorization: Bearer <token>

Alternatively, it can be included in the x-access-token header:

x-access-token: <token>
Substack Icon

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

Subscribe

3. Project Overview#

We'll develop a Node.js Express application that allows users to:

  • Sign Up: Create a new account.
  • Sign In: Authenticate using username and password.
  • Role-Based Access: Access resources based on roles (admin, moderator, user).

Our backend will interact with a MySQL database using Sequelize ORM, ensuring efficient data management and security.

4. Technology Stack#

  • Node.js: Runtime environment.
  • Express 4: Web framework.
  • Sequelize 6: ORM for MySQL.
  • MySQL 8: Relational database.
  • JWT 9: Token-based authentication.
  • bcryptjs 2: Password hashing.
  • CORS 2: Cross-Origin Resource Sharing.
Demo Icon

Want to try passkeys yourself? Check our Passkeys Demo.

Try Passkeys

5. Project Structure#

Here's the directory structure for our application:

node-js-jwt-auth/ ├── 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 ├── package.json └── server.js

6. Setting Up the Node.js Application#

6.1. Initialize the Project#

Create a new directory and initialize the Node.js application:

mkdir node-js-jwt-auth cd node-js-jwt-auth npm init -y

6.2. Install Dependencies#

Install the necessary packages:

npm install express sequelize mysql2 cors jsonwebtoken bcryptjs

6.3. Configure package.json#

Ensure your package.json includes the following script:

"scripts": { "start": "node server.js" }
Slack Icon

Become part of our Passkeys Community for updates and support.

Join

7. Building the Express Server#

7.1. Create server.js#

Using ESModule syntax, set up the Express server:

// 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)); app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Simple test route app.get('/', (req, res) => { res.json({ message: 'Welcome to the Node.js JWT Authentication API.' }); }); // Routes app.use('/api/auth', authRoutes); app.use('/api/test', userRoutes); // Set port and listen for requests const PORT = process.env.PORT || 8080; db.sequelize.sync({ force: false }).then(() => { console.log('Database synchronized'); app.listen(PORT, () => { console.log(`Server is running on port ${PORT}.`); }); });

Note: Ensure that your package.json includes "type": "module" to enable ESModule syntax.

7.2. Update package.json for ESModules#

{ ... "type": "module", ... }
Analyzer Icon

Are your users passkey-ready?

Test Passkey-Readiness

8. Configuring the Database#

8.1. Create cofiguration file#

Set up your MySQL database configuration in app/config/db.config.js:

// app/config/db.config.js export default { HOST: 'localhost', USER: 'root', PASSWORD: 'root', DB: 'db', PORT: 8081, dialect: 'mysql', pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } };

8.2. Initialize Sequelize#

Create app/models/index.js to initialize Sequelize and define model associations:

// 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, port: dbConfig.PORT, }); 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', foreignKey: 'roleId', otherKey: 'userId', }); db.user.belongsToMany(db.role, { through: 'user_roles', foreignKey: 'userId', otherKey: 'roleId', as: 'roles', }); db.ROLES = ['user', 'admin', 'moderator']; export default db;

8.3. Define Models#

8.3.1. User Model#

// app/models/user.model.js export default (sequelize, Sequelize) => { const User = sequelize.define('users', { username: { type: Sequelize.STRING, unique: true }, email: { type: Sequelize.STRING, unique: true }, password: { type: Sequelize.STRING } }); return User; };

8.3.2. Role Model#

// app/models/role.model.js export default (sequelize, Sequelize) => { const Role = sequelize.define('roles', { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: Sequelize.STRING } }); return Role; };

8.4 Local MySQL Database#

If you don't already have some MySQL database for testing, you can create one for your testing environment using the following docker command:

docker run --rm -p 8081:3306 \ -e MYSQL_ROOT_PASSWORD=root \ -e MYSQL_DATABASE=db \ mysql:8

9. Setting Up Authentication Configuration#

9.1. Create a configuration file#

Create a configuration file under app/config/auth.config.js:

// app/config/auth.config.js export default { secret: 'your-secret-key' };

Security Note: Replace 'your-secret-key' with a strong, unpredictable secret. Consider using environment variables to manage sensitive information.

Debugger Icon

Want to experiment with passkey flows? Try our Passkeys Debugger.

Try for Free

10. Implementing Middleware Functions#

10.1. Verify Sign-Up Middleware#

Checks for duplicate usernames or emails and validates roles.

// app/middlewares/verifySignUp.js import db from '../models/index.js'; const { ROLES, user: User } = db; export const checkDuplicateUsernameOrEmail = async (req, res, next) => { try { const userByUsername = await User.findOne({ where: { username: req.body.username } }); if (userByUsername) { return res.status(400).json({ message: 'Username is already in use!' }); } const userByEmail = await User.findOne({ where: { email: req.body.email } }); if (userByEmail) { return res.status(400).json({ message: 'Email is already in use!' }); } next(); } catch (error) { res.status(500).json({ message: error.message }); } }; export 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: `Role ${role} does not exist!` }); } } } next(); };

10.2. JWT Authentication Middleware#

Verifies tokens and checks user roles.

// app/middlewares/authJwt.js import jwt from 'jsonwebtoken'; import db from '../models/index.js'; import authConfig from '../config/auth.config.js'; const { user: User, role: Role } = db; export const verifyToken = async (req, res, next) => { const token = req.headers['x-access-token'] || req.headers['authorization']; if (!token) { return res.status(403).json({ message: 'No token provided!' }); } try { const decoded = jwt.verify(token.replace('Bearer ', ''), authConfig.secret); req.userId = decoded.id; const user = await User.findByPk(req.userId); if (!user) { return res.status(401).json({ message: 'Unauthorized!' }); } next(); } catch (err) { return res.status(401).json({ message: 'Unauthorized!' }); } }; export const isAdmin = async (req, res, next) => { try { const user = await User.findByPk(req.userId); const roles = await user.getRoles(); const adminRole = roles.find(role => role.name === 'admin'); if (adminRole) { next(); return; } res.status(403).json({ message: 'Require Admin Role!' }); } catch (error) { res.status(500).json({ message: error.message }); } }; export const isModerator = async (req, res, next) => { try { const user = await User.findByPk(req.userId); const roles = await user.getRoles(); const modRole = roles.find(role => role.name === 'moderator'); if (modRole) { next(); return; } res.status(403).json({ message: 'Require Moderator Role!' }); } catch (error) { res.status(500).json({ message: error.message }); } }; export const isModeratorOrAdmin = async (req, res, next) => { try { const user = await User.findByPk(req.userId); const roles = await user.getRoles(); const hasRole = roles.some(role => ['admin', 'moderator'].includes(role.name)); if (hasRole) { next(); return; } res.status(403).json({ message: 'Require Moderator or Admin Role!' }); } catch (error) { res.status(500).json({ message: error.message }); } };

10.3. Export Middleware#

// app/middlewares/index.js import * as authJwt from './authJwt.js'; import { checkDuplicateUsernameOrEmail, checkRolesExisted } from './verifySignUp.js'; export { authJwt, checkDuplicateUsernameOrEmail, checkRolesExisted };

11. Creating Controllers#

11.1. Authentication Controller#

Handles user signup and signin.

// app/controllers/auth.controller.js import db from '../models/index.js'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import authConfig from '../config/auth.config.js'; const { user: User, role: Role } = db; export const signup = async (req, res) => { try { const { username, email, password, roles } = req.body; const hashedPassword = await bcrypt.hash(password, 8); const userRole = await Role.findOne({ where: { name: 'user' } }); const user = await User.create({ username, email, password: hashedPassword, }); await user.setRoles([userRole]); res.status(201).json({ message: 'User registered successfully!' }); } catch (error) { res.status(500).json({ message: error.message }); } }; export const signin = async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ where: { username }, include: { model: Role, as: 'roles' } }); if (!user) { return res.status(404).json({ message: 'User Not found.' }); } const passwordIsValid = await bcrypt.compare(password, user.password); if (!passwordIsValid) { return res.status(401).json({ accessToken: null, message: 'Invalid Password!' }); } const token = jwt.sign({ id: user.id }, authConfig.secret, { expiresIn: 86400 // 24 hours }); const authorities = user.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 }); } };

11.2. User Controller#

Handles access to protected resources.

// 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 adminBoard = (req, res) => { res.status(200).send('Admin Content.'); }; export const moderatorBoard = (req, res) => { res.status(200).send('Moderator Content.'); };
StateOfPasskeys Icon

Want to find out how many people can use passkeys?

View Adoption Data

12. Defining Routes#

12.1. Authentication Routes#

// app/routes/auth.routes.js import express from 'express'; import { signup, signin } from '../controllers/auth.controller.js'; import { checkDuplicateUsernameOrEmail, checkRolesExisted } from '../middlewares/verifySignUp.js'; const router = express.Router(); router.post( '/signup', [checkDuplicateUsernameOrEmail, checkRolesExisted], signup ); router.post('/signin', signin); export default router;

12.2. User Routes#

// app/routes/user.routes.js import express from 'express'; import { allAccess, userBoard, adminBoard, moderatorBoard } from '../controllers/user.controller.js'; import { verifyToken, isAdmin, isModerator, isModeratorOrAdmin } from '../middlewares/authJwt.js'; const router = express.Router(); router.get('/all', allAccess); router.get('/user', [verifyToken], userBoard); router.get('/mod', [verifyToken, isModerator], moderatorBoard); router.get('/admin', [verifyToken, isAdmin], adminBoard); export default router;

13. Running the Application#

13.1. Start the Server#

Use the following command to start your server:

npm start

13.2. Testing with Postman#

You can use Postman to test the API endpoints.

13.2.1. Signup a User#

Endpoint: POST /api/auth/signup

Body:

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

Note: All users are assigned the user role by default. More privileged roles should not be obtained through registration, but directly through database access.

13.2.2. Signin a User#

Endpoint: POST /api/auth/signin

Body:

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

13.2.3. Access Protected Routes#

Use the received accessToken in the Authorization header:

Authorization: Bearer <accessToken>

Endpoints:

  • GET /api/test/all - Public
  • GET /api/test/user - User, Moderator, Admin
  • GET /api/test/mod - Moderator
  • GET /api/test/admin - Admin

14. Security Best Practices#

  • Secrets Management: Store sensitive information like JWT secrets and database credentials in environment variables. Utilize packages like dotenv to manage these variables securely.

    npm install dotenv

    Example:

    // server.js import dotenv from 'dotenv'; dotenv.config(); // Access variables using process.env.VARIABLE_NAME
  • Token Expiry and Refresh Tokens: Implement refresh tokens to maintain user sessions without compromising security. This involves issuing short-lived access tokens and longer-lived refresh tokens.

  • Secure HTTP Headers: Use middleware like helmet to set secure HTTP headers.

    npm install helmet
    import helmet from 'helmet'; app.use(helmet());
  • Input Validation: Validate all incoming data to prevent attacks. Consider using libraries like express-validator and integrating with your front-end forms.

15. Conclusion#

Congratulations! You've successfully built a secure and robust Node.js Express REST API with JWT authentication. This setup provides a solid foundation for developing scalable and secure backend services.

Next Steps:

  • Implement Refresh Tokens: Enhance security by allowing users to refresh their tokens without re-authenticating.
  • Integrate Frontend: Pair your backend with modern frontend frameworks like React, Vue.js, or Angular.
  • Deploy to Production: Use platforms like Heroku, Vercel, or AWS to host your application.

Happy coding!

16. Further Reading#

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