← Back to DevBytes

Sequelize Authentication: JWT, Sessions, and OAuth Integration

Sequelize Authentication: JWT, Sessions, and OAuth Integration

Authentication is one of the most critical aspects of any modern web application. When you're building an application with Sequelize as your ORM, you need a robust strategy for verifying user identities, managing credentials, and integrating with third-party providers. This tutorial walks you through three of the most common authentication approaches—JSON Web Tokens (JWT), server-side sessions, and OAuth—and shows you how to implement each one cleanly with Sequelize.

What Is Sequelize Authentication?

Sequelize authentication refers to the patterns and techniques used to verify user identity in applications where Sequelize manages the database layer. Sequelize itself is not an authentication library; rather, it provides the data models—such as User, Session, or OAuthAccount—that authentication middleware relies on. The actual authentication logic is typically handled by libraries like jsonwebtoken, express-session, passport, or bcrypt, all of which interact with Sequelize models to persist and retrieve authentication-related data.

Why It Matters

A poorly implemented authentication system can expose your entire application to attackers. Storing passwords in plaintext, leaking session tokens, or failing to validate OAuth callbacks can lead to account takeovers and data breaches. By integrating authentication carefully with Sequelize, you ensure that credentials are stored securely, queries are protected against injection attacks, and user state is managed consistently across requests. Additionally, choosing the right authentication strategy—JWT for stateless APIs, sessions for traditional web apps, or OAuth for delegated access—directly affects scalability, security, and user experience.

Setting Up the Sequelize User Model

Before implementing any authentication strategy, you need a solid User model. This model will store hashed passwords and provide instance methods for validating credentials. The key principle here is to never store raw passwords—always hash them using a strong algorithm like bcrypt.

Defining the Model

// models/user.js
const { DataTypes } = require('sequelize');
const bcrypt = require('bcrypt');

module.exports = (sequelize) => {
  const User = sequelize.define('User', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true,
    },
    email: {
      type: DataTypes.STRING,
      allowNull: false,
      unique: true,
      validate: {
        isEmail: true,
      },
    },
    password: {
      type: DataTypes.STRING,
      allowNull: true, // nullable for OAuth-only users
    },
    name: {
      type: DataTypes.STRING,
      allowNull: true,
    },
    role: {
      type: DataTypes.ENUM('user', 'admin'),
      defaultValue: 'user',
    },
  }, {
    hooks: {
      beforeSave: async (user) => {
        if (user.changed('password') && user.password) {
          const salt = await bcrypt.genSalt(12);
          user.password = await bcrypt.hash(user.password, salt);
        }
      },
    },
  });

  User.prototype.comparePassword = async function (candidatePassword) {
    if (!this.password) return false;
    return bcrypt.compare(candidatePassword, this.password);
  };

  User.prototype.toJSON = function () {
    const values = { ...this.get() };
    delete values.password;
    return values;
  };

  return User;
};

Notice the beforeSave hook automatically hashes the password whenever it changes. The comparePassword instance method provides a clean way to verify a candidate password against the stored hash. The toJSON override ensures the password hash is never accidentally serialized into API responses.

JWT Authentication

JSON Web Tokens are the go-to choice for stateless authentication, especially in REST APIs and single-page applications. A JWT encodes a payload—typically the user ID and roles—signed with a secret key. The client sends the token with each request, and the server verifies it without needing to look up a session in the database.

Generating and Verifying Tokens

First, install the required dependencies:

npm install jsonwebtoken bcrypt

Next, create a utility module for token operations:

// utils/jwt.js
const jwt = require('jsonwebtoken');

const ACCESS_TOKEN_SECRET = process.env.JWT_ACCESS_SECRET;
const REFRESH_TOKEN_SECRET = process.env.JWT_REFRESH_SECRET;

function generateAccessToken(user) {
  return jwt.sign(
    { id: user.id, email: user.email, role: user.role },
    ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
}

function generateRefreshToken(user) {
  return jwt.sign(
    { id: user.id },
    REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
}

function verifyAccessToken(token) {
  return jwt.verify(token, ACCESS_TOKEN_SECRET);
}

function verifyRefreshToken(token) {
  return jwt.verify(token, REFRESH_TOKEN_SECRET);
}

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken,
};

Implementing the Auth Controller

// controllers/authController.js
const { User } = require('../models');
const jwt = require('../utils/jwt');

exports.register = async (req, res) => {
  try {
    const { email, password, name } = req.body;
    const existing = await User.findOne({ where: { email } });
    if (existing) {
      return res.status(409).json({ error: 'Email already registered' });
    }
    const user = await User.create({ email, password, name });
    const accessToken = jwt.generateAccessToken(user);
    const refreshToken = jwt.generateRefreshToken(user);
    return res.status(201).json({ user, accessToken, refreshToken });
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }
};

exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ where: { email } });
    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    const accessToken = jwt.generateAccessToken(user);
    const refreshToken = jwt.generateRefreshToken(user);
    return res.json({ user, accessToken, refreshToken });
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }
};

exports.refresh = async (req, res) => {
  try {
    const { refreshToken } = req.body;
    const payload = jwt.verifyRefreshToken(refreshToken);
    const user = await User.findByPk(payload.id);
    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }
    const newAccessToken = jwt.generateAccessToken(user);
    return res.json({ accessToken: newAccessToken });
  } catch (err) {
    return res.status(401).json({ error: 'Invalid refresh token' });
  }
};

Protecting Routes with JWT Middleware

// middleware/authenticate.js
const jwt = require('../utils/jwt');

module.exports = (req, res, next) => {
  const header = req.headers.authorization;
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing authorization header' });
  }
  const token = header.split(' ')[1];
  try {
    const payload = jwt.verifyAccessToken(token);
    req.user = payload;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

You can then apply this middleware to any protected route:

// routes/profile.js
const express = require('express');
const authenticate = require('../middleware/authenticate');
const { User } = require('../models');

const router = express.Router();

router.get('/profile', authenticate, async (req, res) => {
  const user = await User.findByPk(req.user.id);
  res.json({ user });
});

module.exports = router;

Session-Based Authentication

Session-based authentication stores user state on the server, typically in a database or in-memory store like Redis. The client receives a session ID via a cookie, and the server uses that ID to look up the session on each request. This approach is well-suited for traditional server-rendered applications where you want to invalidate sessions centrally.

Creating a Session Model in Sequelize

If you want to store sessions in your database rather than in memory, you can create a Sequelize-backed session store. First, define a Session model:

// models/session.js
const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
  const Session = sequelize.define('Session', {
    sid: {
      type: DataTypes.STRING,
      primaryKey: true,
    },
    expires: {
      type: DataTypes.DATE,
      allowNull: false,
    },
    data: {
      type: DataTypes.TEXT,
      allowNull: true,
    },
  }, {
    indexes: [
      { fields: ['expires'] },
    ],
  });

  return Session;
};

Configuring Express Session

Install the necessary packages:

npm install express-session connect-session-sequelize

Then wire up the session middleware with your Sequelize store:

// app.js
const express = require('express');
const session = require('express-session');
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const { sequelize } = require('./models');

const app = express();

const sessionStore = new SequelizeStore({
  db: sequelize,
});

app.use(session({
  secret: process.env.SESSION_SECRET,
  store: sessionStore,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
  },
}));

module.exports = app;

Login and Logout with Sessions

// controllers/sessionAuthController.js
const { User } = require('../models');

exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;
    const user = await User.findOne({ where: { email } });
    if (!user || !(await user.comparePassword(password))) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    req.session.userId = user.id;
    req.session.role = user.role;
    return res.json({ user });
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }
};

exports.logout = (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    res.clearCookie('connect.sid');
    return res.json({ message: 'Logged out successfully' });
  });
};

exports.me = async (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  const user = await User.findByPk(req.session.userId);
  return res.json({ user });
};

Session Authentication Middleware

// middleware/requireSession.js
const { User } = require('../models');

module.exports = async (req, res, next) => {
  if (!req.session || !req.session.userId) {
    return res.status(401).json({ error: 'Authentication required' });
  }
  try {
    const user = await User.findByPk(req.session.userId);
    if (!user) {
      return res.status(401).json({ error: 'User not found' });
    }
    req.user = user;
    next();
  } catch (err) {
    return res.status(500).json({ error: err.message });
  }
};

OAuth Integration

OAuth allows users to authenticate using third-party providers like Google, GitHub, or Facebook. Instead of managing passwords yourself, you delegate authentication to the provider and receive a profile that you map to a local user. Passport.js is the most popular Node.js library for handling OAuth flows.

Installing Passport and Strategies

npm install passport passport-google-oauth20

Creating an OAuth Account Model

To support multiple providers per user, create a separate OAuthAccount model that links to your User model:

// models/oauthAccount.js
const { DataTypes } = require('sequelize');

module.exports = (sequelize) => {
  const OAuthAccount = sequelize.define('OAuthAccount', {
    id: {
      type: DataTypes.UUID,
      defaultValue: DataTypes.UUIDV4,
      primaryKey: true,
    },
    provider: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    providerUserId: {
      type: DataTypes.STRING,
      allowNull: false,
    },
    accessToken: {
      type: DataTypes.TEXT,
      allowNull: true,
    },
    refreshToken: {
      type: DataTypes.TEXT,
      allowNull: true,
    },
  }, {
    indexes: [
      { unique: true, fields: ['provider', 'providerUserId'] },
    ],
  });

  return OAuthAccount;
};

Set up the associations in your model index file:

// models/index.js
const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize(process.env.DATABASE_URL);

const userModel = require('./user')(sequelize);
const sessionModel = require('./session')(sequelize);
const oauthAccountModel = require('./oauthAccount')(sequelize);

const User = userModel;
const Session = sessionModel;
const OAuthAccount = oauthAccountModel;

User.hasMany(OAuthAccount, { foreignKey: 'userId', as: 'oauthAccounts' });
OAuthAccount.belongsTo(User, { foreignKey: 'userId', as: 'user' });

module.exports = { sequelize, User, Session, OAuthAccount };

Configuring the Google OAuth Strategy

// config/passport.js
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const { User, OAuthAccount } = require('../models');

passport.use(new GoogleStrategy({
  clientID: process.env.GOOGLE_CLIENT_ID,
  clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  callbackURL: '/auth/google/callback',
}, async (accessToken, refreshToken, profile, done) => {
  try {
    // Check if an OAuth account already exists
    let oauthAccount = await OAuthAccount.findOne({
      where: { provider: 'google', providerUserId: profile.id },
      include: [{ model: User, as: 'user' }],
    });

    if (oauthAccount) {
      // Update tokens
      oauthAccount.accessToken = accessToken;
      oauthAccount.refreshToken = refreshToken;
      await oauthAccount.save();
      return done(null, oauthAccount.user);
    }

    // Otherwise, find or create a local user by email
    const email = profile.emails[0].value;
    let user = await User.findOne({ where: { email } });

    if (!user) {
      user = await User.create({
        email,
        name: profile.displayName,
        password: null,
      });
    }

    // Link the OAuth account
    await OAuthAccount.create({
      provider: 'google',
      providerUserId: profile.id,
      accessToken,
      refreshToken,
      userId: user.id,
    });

    return done(null, user);
  } catch (err) {
    return done(err, null);
  }
}));

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  try {
    const user = await User.findByPk(id);
    done(null, user);
  } catch (err) {
    done(err, null);
  }
});

module.exports = passport;

Defining OAuth Routes

// routes/oauth.js
const express = require('express');
const passport = require('../config/passport');
const jwt = require('../utils/jwt');

const router = express.Router();

router.get('/google', passport.authenticate('google', {
  scope: ['profile', 'email'],
}));

router.get('/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    // Issue JWT for the authenticated user
    const accessToken = jwt.generateAccessToken(req.user);
    const refreshToken = jwt.generateRefreshToken(req.user);
    // Redirect to frontend with tokens
    const redirectUrl = `${process.env.FRONTEND_URL}/auth/callback?accessToken=${accessToken}&refreshToken=${refreshToken}`;
    res.redirect(redirectUrl);
  }
);

module.exports = router;

Don't forget to initialize Passport in your main app file:

// app.js (additions)
const passport = require('./config/passport');

app.use(passport.initialize());
app.use(passport.session());

Best Practices

Regardless of which authentication strategy you choose, following established best practices will keep your application secure and maintainable.

Conclusion

Authentication in a Sequelize-based application is a matter of combining the right libraries with well-designed models and middleware. JWT provides a stateless solution ideal for APIs, sessions offer centralized control for traditional web apps, and OAuth lets you leverage trusted third-party providers. By building a secure User model with bcrypt hashing, choosing the appropriate strategy for your use case, and following security best practices like short-lived tokens, secure cookies, and input validation, you can create an authentication system that is both robust and maintainable. The key is to treat authentication as a first-class concern in your architecture rather than an afterthought, ensuring that every layer—from the database schema to the HTTP middleware—works together to protect your users.

— Ad —

Google AdSense will appear here after approval

← Back to all articles