nodejsnotes

Topic 005: Authentication using passportJS + JWT & passportJS + Oauth in Node.jS

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.

Authentication using Passport.js with JWT:

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.

  1. 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);
      }
    }
  )
);
  1. 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");
});

Authentication using Passport.js with OAuth:

OAuth is a standard protocol for authorization, allowing users to log in to third-party services without exposing their credentials to the application.

  1. 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);
      }
    }
  )
);
  1. 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.


1. JWT verify method to check the validity

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.

Step-by-Step Guide

  1. 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
    
  2. Install Dependencies

    Install the jsonwebtoken package:

    npm install jsonwebtoken
    
  3. 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,
    };
    
  4. 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);
    

Running the Code

  1. Run the Test File

    Execute the test.js file to see the JWT functionalities in action:

    node test.js
    

Explanation

  1. Generating a Token

    • generateToken function creates a JWT with the given payload and expiration time using jwt.sign.
  2. Verifying a Token

    • verifyToken function verifies the JWT using jwt.verify. If the token is invalid or expired, it throws an error.
  3. Decoding a Token

    • decodeToken function decodes the JWT without verifying its signature using jwt.decode.

2. JWT custom logic to check the validity

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:

  1. 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,
    };
    
  2. 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);
    }
    

Explanation

  1. 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”.
  2. Testing the Expiration Check

    • The test file now includes a case to generate an expired token and verify it using the updated 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);
              });
            }
          });
        });
      }
    )
  );
};