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.
- Setup Passport.js and JWT Strategy:
// 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);
}
}
)
);- Initialize Passport.js in your application:
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.
- Setup Passport.js with OAuth Strategy (e.g., Google OAuth):
// 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);
}
}
)
);- Set up OAuth routes and initialize Passport.js:
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
jsonwebtokenpackage:npm install jsonwebtoken
-
Create the JWT Utility File
Create a new file named
jwtUtil.jsto 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.jsto 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.jsfile to see the JWT functionalities in action:node test.js
-
Generating a Token
generateTokenfunction creates a JWT with the given payload and expiration time usingjwt.sign.
-
Verifying a Token
verifyTokenfunction verifies the JWT usingjwt.verify. If the token is invalid or expired, it throws an error.
-
Decoding a Token
decodeTokenfunction decodes the JWT without verifying its signature usingjwt.decode.
- This implementation provides a basic setup for generating, verifying, and decoding JWTs in a Node.js environment. Adjust the
SECRET_KEYand 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
verifyTokenUpdate the
verifyTokenfunction injwtUtil.jsto 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
verifyTokenfunction intest.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- After decoding the token using
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). - If the token is expired, it throws an error with the message "Token is expired".
- After decoding the token using
-
Testing the Expiration Check
- The test file now includes a case to generate an expired token and verify it using the updated
verifyTokenfunction to ensure the manual expiration check works correctly.
- The test file now includes a case to generate an expired token and verify it using the updated
- This setup ensures that tokens are verified not only for their signature but also for their expiration, adding an extra layer of validation.
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);
});
}
});
});
}
)
);
};