Topic 005: Authentication using passportJS + JWT & passportJS + Oauth in Node.jS
let’s first start with explaining Authentication using Passport.js with JWT (JSON Web Tokens), and then we’ll cover Passport.js with OAuth.
Passport.js is a popular authentication middleware for Node.js applications. When combined with JWT, it provides a stateless authentication mechanism where the server doesn’t need to keep track of session data.
// Import required modules
const passport = require("passport");
const { Strategy: JwtStrategy, ExtractJwt } = require("passport-jwt");
const User = require("./models/User"); // Your user model
// Configure JWT strategy
passport.use(
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: "your_jwt_secret", // Replace with your JWT secret key
},
async (jwtPayload, done) => {
try {
const user = await User.findById(jwtPayload.sub);
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}
)
);
const express = require("express");
const passport = require("passport");
const app = express();
// Initialize Passport middleware
app.use(passport.initialize());
// Routes requiring authentication
app.get("/profile", passport.authenticate("jwt", { session: false }), (req, res) => {
res.json(req.user);
});
// Start your server
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
OAuth is a standard protocol for authorization, allowing users to log in to third-party services without exposing their credentials to the application.
// Import required modules
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
const User = require("./models/User"); // Your user model
// Configure Google OAuth strategy
passport.use(
new GoogleStrategy(
{
clientID: "your_google_client_id",
clientSecret: "your_google_client_secret",
callbackURL: "/auth/google/callback",
},
async (accessToken, refreshToken, profile, done) => {
try {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
// Create new user if not found
user = new User({ googleId: profile.id, displayName: profile.displayName });
await user.save();
}
return done(null, user);
} catch (error) {
return done(error, false);
}
}
)
);
const express = require("express");
const passport = require("passport");
const app = express();
// Initialize Passport middleware
app.use(passport.initialize());
// Google OAuth login route
app.get("/auth/google", passport.authenticate("google", { scope: ["profile"] }));
// Google OAuth callback route
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/login" }),
(req, res) => {
// Successful authentication, redirect to profile page
res.redirect("/profile");
}
);
// Profile route after successful authentication
app.get("/profile", (req, res) => {
res.json(req.user);
});
// Start your server
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
These code snippets demonstrate how to set up authentication using Passport.js with JWT and OAuth (Google OAuth in this case). Make sure to replace placeholders like your_jwt_secret
, your_google_client_id
, and your_google_client_secret
with actual values from your application setup. Additionally, replace User
with your actual user model and adjust routes and callbacks as needed for your application.
To implement JWT (JSON Web Token) verification and decoding in JavaScript, you’ll need to create a Node.js environment. This involves using the jsonwebtoken
package. Below is a comprehensive guide to set up and implement the JWT functionality, including verification and decoding.
Initialize Node.js Project
First, create a new directory for your project and initialize a Node.js project:
mkdir jwt-example
cd jwt-example
npm init -y
Install Dependencies
Install the jsonwebtoken
package:
npm install jsonwebtoken
Create the JWT Utility File
Create a new file named jwtUtil.js
to implement the JWT functionalities.
// jwtUtil.js
const jwt = require("jsonwebtoken");
const SECRET_KEY = "your-256-bit-secret"; // Replace with your secret key
/**
* Generate a JWT token
* @param {Object} payload - The payload to sign
* @param {String} expiresIn - Token expiration time
* @returns {String} - JWT token
*/
function generateToken(payload, expiresIn = "1h") {
return jwt.sign(payload, SECRET_KEY, { expiresIn });
}
/**
* Verify a JWT token
* @param {String} token - JWT token to verify
* @returns {Object} - Decoded payload if the token is valid
* @throws {Error} - If the token is invalid or expired
*/
function verifyToken(token) {
try {
return jwt.verify(token, SECRET_KEY);
} catch (error) {
throw new Error("Token is invalid or expired");
}
}
/**
* Decode a JWT token without verification
* @param {String} token - JWT token to decode
* @returns {Object} - Decoded payload
*/
function decodeToken(token) {
return jwt.decode(token);
}
module.exports = {
generateToken,
verifyToken,
decodeToken,
};
Create a Test File
Create a file named test.js
to test your JWT utility functions.
// test.js
const { generateToken, verifyToken, decodeToken } = require("./jwtUtil");
// Sample payload
const payload = {
userId: 123,
username: "testuser",
};
// Generate a token
const token = generateToken(payload);
console.log("Generated Token:", token);
// Verify the token
try {
const verifiedPayload = verifyToken(token);
console.log("Verified Payload:", verifiedPayload);
} catch (error) {
console.error("Verification Error:", error.message);
}
// Decode the token without verification
const decodedPayload = decodeToken(token);
console.log("Decoded Payload:", decodedPayload);
Run the Test File
Execute the test.js
file to see the JWT functionalities in action:
node test.js
Generating a Token
generateToken
function creates a JWT with the given payload and expiration time using jwt.sign
.Verifying a Token
verifyToken
function verifies the JWT using jwt.verify
. If the token is invalid or expired, it throws an error.Decoding a Token
decodeToken
function decodes the JWT without verifying its signature using jwt.decode
.SECRET_KEY
and other configurations according to your application’s requirements.The condition if (Date.now() >= exp * 1000) { return false; }
is used to manually check if a JWT token has expired by comparing the current time with the token’s expiration time (exp
). This can be useful if you want to implement an additional expiration check without using the built-in verification method provided by the jsonwebtoken
package.
Here’s how you can incorporate this manual expiration check into the JWT utility functions:
Add Manual Expiration Check in verifyToken
Update the verifyToken
function in jwtUtil.js
to include the manual expiration check.
// jwtUtil.js
const jwt = require("jsonwebtoken");
const SECRET_KEY = "your-256-bit-secret"; // Replace with your secret key
/**
* Generate a JWT token
* @param {Object} payload - The payload to sign
* @param {String} expiresIn - Token expiration time
* @returns {String} - JWT token
*/
function generateToken(payload, expiresIn = "1h") {
return jwt.sign(payload, SECRET_KEY, { expiresIn });
}
/**
* Verify a JWT token with manual expiration check
* @param {String} token - JWT token to verify
* @returns {Object} - Decoded payload if the token is valid
* @throws {Error} - If the token is invalid or expired
*/
function verifyToken(token) {
try {
const decoded = jwt.verify(token, SECRET_KEY);
// Manual expiration check
if (Date.now() >= decoded.exp * 1000) {
throw new Error("Token is expired");
}
return decoded;
} catch (error) {
throw new Error("Token is invalid or expired");
}
}
/**
* Decode a JWT token without verification
* @param {String} token - JWT token to decode
* @returns {Object} - Decoded payload
*/
function decodeToken(token) {
return jwt.decode(token);
}
module.exports = {
generateToken,
verifyToken,
decodeToken,
};
Update the Test File
Make sure to test the updated verifyToken
function in test.js
.
// test.js
const { generateToken, verifyToken, decodeToken } = require("./jwtUtil");
// Sample payload
const payload = {
userId: 123,
username: "testuser",
};
// Generate a token
const token = generateToken(payload);
console.log("Generated Token:", token);
// Verify the token
try {
const verifiedPayload = verifyToken(token);
console.log("Verified Payload:", verifiedPayload);
} catch (error) {
console.error("Verification Error:", error.message);
}
// Decode the token without verification
const decodedPayload = decodeToken(token);
console.log("Decoded Payload:", decodedPayload);
// Simulate an expired token for testing
const expiredToken = generateToken(payload, "-10s"); // Token expired 10 seconds ago
try {
const verifiedExpiredPayload = verifyToken(expiredToken);
console.log("Verified Expired Payload:", verifiedExpiredPayload);
} catch (error) {
console.error("Verification Error (Expired Token):", error.message);
}
Manual Expiration Check in verifyToken
jwt.verify
, the function checks if the current time (Date.now()
) is greater than or equal to the expiration time (exp
) multiplied by 1000 (to convert seconds to milliseconds).Testing the Expiration Check
verifyToken
function to ensure the manual expiration check works correctly.passport.js
let LocalStrategy = require("passport-local").Strategy;
let FacebookStrategy = require("passport-facebook").Strategy;
let GoogleStrategy = require("passport-google-oauth2").Strategy;
let JwtStrategy = require("passport-jwt").Strategy,
ExtractJwt = require("passport-jwt").ExtractJwt;
let jwt_secret = require("./config");
let LinkedInStrategy = require("passport-linkedin-oauth2").Strategy;
let User = require("../app/models/user");
module.exports = (passport) => {
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
passport.use(
"local-signup",
new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
passReqToCallback: true,
},
(req, email, password, done) => {
process.nextTick(() => {
User.findOne({ "local.email": email }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, false, { message: "That email is already taken." });
} else {
let newUser = new User();
newUser.local.email = email;
newUser.local.password = newUser.generateHash(password);
newUser.save((err) => {
if (err) throw err;
return done(null, newUser);
});
}
});
});
}
)
);
passport.use(
"local-login",
new LocalStrategy(
{
usernameField: "email",
passwordField: "password",
passReqToCallback: true,
},
(req, email, password, done) => {
User.findOne({ "local.email": email }, (err, user) => {
if (err) return done(err);
if (!user) return done(null, false, { message: "Incorrect username." });
if (!user.validPassword(password))
return done(null, false, { message: "Incorrect password." });
return done(null, user);
});
}
)
);
// Passport JWT Strategy
passport.use(
"jwt-auth",
new JwtStrategy(
{
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwt_secret.secret,
},
(jwt_payload, done) => {
User.findOne({ "local.email": jwt_payload.email }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user, { message: "A user was found thanks to the jwt token" });
} else {
return done(null, false, { message: "No user was found thanks to the jwt token" });
}
});
}
)
);
// Passport Facebook Strategy
passport.use(
new FacebookStrategy(
{
clientID: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackURL: process.env.FACEBOOK_CALLBACK_URL,
profileFields: ["id", "emails", "name"],
},
(token, refreshToken, profile, done) => {
// asynchronous
process.nextTick(() => {
User.findOne({ "facebook.id": profile.id }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user);
} else {
let newUser = new User();
newUser.facebook.id = profile.id;
console.log("fb-token", token);
newUser.facebook.token = token;
newUser.facebook.name = profile.name.givenName + " " + profile.name.familyName;
newUser.facebook.email = profile.emails[0].value;
newUser.save((err) => {
if (err) throw err;
return done(null, newUser);
});
}
});
});
}
)
);
// Passport Google Strategy
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
},
(token, refreshToken, profile, done) => {
process.nextTick(() => {
User.findOne({ "google.id": profile.id }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user);
} else {
let newUser = new User();
newUser.google.id = profile.id;
newUser.google.token = token;
newUser.google.name = profile.displayName;
newUser.google.email = profile.emails[0].value;
newUser.save((err) => {
if (err) throw err;
return done(null, newUser);
});
}
});
});
}
)
);
// Linkedin Strategy
passport.use(
new LinkedInStrategy(
{
clientID: process.env.LINKEDIN_CLIENT_ID,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET,
callbackURL: process.env.LINKEDIN_CALLBACK_URL,
scope: ["r_emailaddress", "r_liteprofile"],
},
(token, refreshToken, profile, done) => {
process.nextTick(() => {
User.findOne({ "linkedin.id": profile.id }, (err, user) => {
if (err) return done(err);
if (user) {
return done(null, user);
} else {
let newUser = new User();
newUser.linkedin.id = profile.id;
newUser.linkedin.token = token;
newUser.linkedin.name = profile.displayName;
newUser.linkedin.email = profile.emails[0].value;
newUser.save((err) => {
if (err) throw err;
return done(null, newUser);
});
}
});
});
}
)
);
};