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.
- Always hash passwords with bcrypt using a cost factor of at least 12. Never store plaintext passwords, and never use weak hashing algorithms like MD5 or SHA-1.
- Use environment variables for secrets. Store JWT secrets, session secrets, and OAuth client secrets in environment variables or a secrets manager—never hardcode them in source files.
- Set short expiration times for access tokens. A 15-minute access token paired with a 7-day refresh token is a common and secure pattern.
- Enable secure cookie flags. Set
httpOnly,secure, andsameSiteon session cookies to prevent cross-site scripting and cross-site request forgery attacks. - Validate all input. Use a validation library like
joiorexpress-validatorto sanitize registration and login payloads before they reach your Sequelize models. - Implement rate limiting. Protect login and registration endpoints with rate limiting to mitigate brute-force attacks. Libraries like
express-rate-limitwork well for this. - Separate OAuth accounts from local credentials. Using a dedicated
OAuthAccountmodel lets users link multiple providers and avoids conflicts between password-based and OAuth-based users. - Log authentication events. Track logins, logouts, failed attempts, and OAuth linkages for auditing and anomaly detection.
- Rotate refresh tokens. When a refresh token is used to obtain a new access token, issue a new refresh token as well and invalidate the old one.
- Use HTTPS in production. Authentication tokens and session cookies must travel over encrypted connections to prevent interception.
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.