Learn How to Handle Authentication in Node.js with Passport.js

Authentication is a critical part of most web applications. It allows you to secure your app by restricting access to certain routes or resources only to logged-in users. While authentication can be complex to implement from scratch, using the right tools and following best practices can make the process much smoother.

In this article, we‘ll learn how to handle authentication in a Node.js app using Passport.js, one of the most popular auth libraries. We‘ll go through the process of setting up Passport with a local authentication strategy, implementing user registration and login, securing routes, and even adding support for social logins like Google and Facebook.

By the end of this guide, you‘ll have a solid foundation for adding auth to your own Node.js projects. Let‘s jump in!

What is Passport.js?

Passport is an authentication middleware for Node.js that makes it easy to implement various authentication strategies in an Express-based web app. It‘s very flexible and modular, with support for authenticating using a username and password, as well as via OAuth providers like Facebook, Twitter, Google, etc.

The core concepts in Passport are:

  • Strategies: How you want to authenticate a user (e.g. local strategy using username/password, OAuth, JWT). Passport has 500+ strategies.
  • Sessions: Keeping a user logged in via cookies/sessions
  • Middleware: Passport provides middlewares that hook into the request pipeline to authenticate requests

The nice thing about Passport is that it abstracts a lot of the complexity and you can mix and match various strategies based on your app‘s needs. You can also keep your chosen strategy separate from the rest of your application code for better modularity.

Setting up the project

Before we start implementing auth, let‘s first set up a basic Node/Express project and install the dependencies we‘ll need.

Initialize a new project and install Express, Mongoose to connect to MongoDB for our database, and a few other utility packages:

mkdir passport-tutorial && cd passport-tutorial
npm init -y
npm install express mongoose passport passport-local passport-google-oauth20 passport-facebook passport-twitter connect-mongo express-session cookie-parser

Next, create a file named app.js with the following code to set up a basic Express app:

const express = require(‘express‘);
const mongoose = require(‘mongoose‘);
const session = require(‘express-session‘);
const MongoStore = require(‘connect-mongo‘);
const passport = require(‘passport‘);
const cookieParser = require(‘cookie-parser‘);

const app = express();

// Configure MongoDB connection 
mongoose.connect(‘mongodb://localhost/passport-tutorial‘, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});

// Configure sessions stored in MongoDB
app.use(
  session({
    secret: ‘mysecret‘,
    resave: false,
    saveUninitialized: true,
    store: MongoStore.create({
      mongoUrl: ‘mongodb://localhost/passport-tutorial‘,
    }),
    cookie: {
        maxAge: 1000 * 60 * 60 * 24 // 1 day 
    }
  })
);

// Initialize passport
app.use(passport.initialize());
app.use(passport.session());
app.use(cookieParser());
app.use(express.json());

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server started on port ${PORT}`));

This sets up a basic Express app, connects it to MongoDB, configures sessions with MongoStore, and initializes Passport. We also enable parsing of JSON in requests and set up cookie parsing.

Now let‘s move on to the authentication portion with Passport!

Implementing Local Authentication

The first auth strategy we‘ll implement is local authentication, where users log in with an email/username and password. We‘ll need to:

  1. Create a User model to store user data in MongoDB
  2. Configure the Passport LocalStrategy
  3. Create routes for user registration and login
  4. Secure routes to require authentication

Creating the User Model

Create a new file models/User.js and define the User model with Mongoose:

const mongoose = require(‘mongoose‘);

const UserSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
  },
});

module.exports = mongoose.model(‘User‘, UserSchema);

Here we create a basic User model with an email, hashed password, and createdAt timestamp. The email is marked as required and unique.

Configuring Passport‘s Local Strategy

Next, configure Passport‘s LocalStrategy in a new file config/passport.js:

const LocalStrategy = require(‘passport-local‘).Strategy;
const bcrypt = require(‘bcrypt‘);
const User = require(‘../models/User‘);

module.exports = function(passport) {
  passport.use(
    new LocalStrategy({ usernameField: ‘email‘ }, async (email, password, done) => {
      try {
        // Find the user by email
        const user = await User.findOne({ email });

        // If user not found, return error
        if (!user) {
          return done(null, false, { message: ‘Invalid email or password‘ });
        }

        // Check password
        const isMatch = await bcrypt.compare(password, user.password);

        if (!isMatch) {
          return done(null, false, { message: ‘Invalid email or password‘ });
        }

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

  // Serialize user into session
  passport.serializeUser((user, done) => {
    done(null, user.id);
  });

  // Deserialize user from session  
  passport.deserializeUser(async (id, done) => {
    try {
      const user = await User.findById(id);
      done(null, user);
    } catch (err) {
      done(err);
    }
  });
};

This sets up the LocalStrategy to find a user by their email, validate the password using bcrypt, and either return the authenticated user or an error.

We also define serializeUser/deserializeUser to store the user ID in the session and retrieve the full user on subsequent requests.

Registration and Login Routes

Now let‘s create the routes for user registration and login. Create a new routes file routes/auth.js:

const express = require(‘express‘);
const passport = require(‘passport‘);
const bcrypt = require(‘bcrypt‘);
const User = require(‘../models/User‘);

const router = express.Router();

// Registration route
router.post(‘/register‘, async (req, res) => {
  const { email, password } = req.body;

  try {
    // Check if user already exists
    let user = await User.findOne({ email });

    if (user) {
      return res.status(400).json({ message: ‘User already exists‘ });
    }

    // Create new user
    user = new User({ email, password });

    // Hash the password
    const salt = await bcrypt.genSalt(10);
    user.password = await bcrypt.hash(password, salt);

    // Save user to DB
    await user.save();

    res.json({ message: ‘Registration successful‘ });
  } catch (err) {
    console.error(err);
    res.status(500).json({ message: ‘Server error‘ });
  }
});

// Login route 
router.post(‘/login‘, (req, res, next) => {
  passport.authenticate(‘local‘, (err, user, info) => {
    if (err) {
      return next(err);
    }

    if (!user) {
      return res.status(401).json({ message: info.message });
    }

    req.logIn(user, err => {
      if (err) {
        return next(err);   
      }

      res.json({ message: ‘Login successful‘ });
    });
  })(req, res, next);
});

module.exports = router;

The registration route checks if a user already exists with the given email. If not, it creates a new user, hashes their password with bcrypt, and saves them to the database.

The login route uses Passport‘s authenticate middleware to authenticate the user using the LocalStrategy we defined earlier. If authentication succeeds, the user is logged in (which saves their ID to the session) and a success response is returned. If it fails, a 401 Unauthorized response is returned.

Finally, mount this router in app.js:

// Auth routes
app.use(‘/auth‘, require(‘./routes/auth‘));

Securing Routes

To make routes accessible only to authenticated users, create a simple middleware requireAuth in middleware/auth.js:

module.exports = {
  requireAuth: (req, res, next) => {
    if (req.isAuthenticated()) {
      return next();
    }

    res.status(401).json({ message: ‘Unauthorized‘ });
  },
};

This checks if the request is authenticated using Passport‘s isAuthenticated method, which checks for a user ID in the session. If it exists, the request moves on to the next middleware/route handler. If not, a 401 Unauthorized response is returned.

You can then secure any route by adding this middleware:

const { requireAuth } = require(‘../middleware/auth‘);

router.get(‘/private‘, requireAuth, (req, res) => {
  res.json({ message: ‘This is a private route‘ });  
});

And that covers the basics of local authentication with Passport! Users can now register, login, and access secured routes in your app.

Implementing Social Logins

In addition to local auth, Passport lets you easily add OAuth-based social logins to your app. Let‘s add support for logging in with Google, Facebook, and Twitter.

Google OAuth

First, install the passport-google-oauth20 strategy:

npm install passport-google-oauth20

Then configure it in config/passport.js:

const GoogleStrategy = require(‘passport-google-oauth20‘).Strategy;

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 {
      let user = await User.findOne({ googleId: profile.id });

      if (!user) {
        // Create a new user if one doesn‘t exist
        const newUser = {
          googleId: profile.id,
          email: profile.emails[0].value,
        };

        user = await User.create(newUser);
      }

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

The GoogleStrategy takes your app‘s client ID and secret (obtained from the Google Developer Console), as well as a callback URL.

In the verify callback, it tries to find a user with the given Google ID. If one doesn‘t exist, a new user is created with the ID and email from the Google profile. Finally it calls done with the authenticated user.

The last step is to create the routes for initiating the Google OAuth flow and handling the callback:

// Google auth routes
router.get(‘/google‘, passport.authenticate(‘google‘, { scope: [‘profile‘, ‘email‘] }));

router.get(‘/google/callback‘, passport.authenticate(‘google‘, { failureRedirect: ‘/login‘ }),
  (req, res) => {
    res.redirect(‘/‘);
  }
);  

Visiting the /auth/google route initiates the OAuth flow and prompts the user to log in with their Google account and grant permission to your app.

Once that‘s complete, Google redirects back to the /auth/google/callback route, where Passport authenticates the user using the GoogleStrategy. If successful, it logs the user in and redirects to the homepage. If it fails, it redirects back to the login page.

And that‘s it! Users can now log in to your app using their Google account. The process is very similar for other OAuth providers like Facebook and Twitter – you just need to configure the appropriate strategies with your app‘s credentials and add the routes.

Additional Considerations

While we‘ve covered the core concepts of authentication with Passport.js, there are some additional things to consider in a production app:

  • Password Security: Always hash passwords using a secure algorithm like bcrypt before storing them. Never store plain-text passwords.

  • Password Reset: Implement a secure password reset flow using temporary tokens sent via email.

  • Account Lockout: Lock accounts after too many failed login attempts to prevent brute-force attacks.

  • CSRF Protection: Implement CSRF protection on all forms that perform state-changing actions like login and registration.

  • HTTPS: Always use HTTPS in production to encrypt all traffic, including sensitive data like passwords.

  • Session Security: Generate secure session secrets, use secure cookies, and consider storing sessions in a database like Redis for better scalability.

  • Two-Factor Authentication: For an extra layer of security, consider implementing 2FA using SMS or TOTP.

By following these best practices, you can ensure your app‘s authentication is secure and protects your users‘ data.

Conclusion

In this article, we learned how to handle authentication in a Node.js app using Passport.js. We implemented local authentication with email/password, secured routes with middleware, added support for social logins with OAuth, and discussed some additional security considerations.

Passport.js makes it easy to add robust, secure authentication to your Node apps. It abstracts a lot of the complexity, provides a simple and modular API, and supports a wide variety of authentication strategies.

I hope this guide gave you a solid foundation for working with Passport.js and authentication in Node. The concepts we covered can be extended to add auth to any Node app, whether it‘s a web app, API, or CLI tool.

For further reading, check out the official Passport.js docs, the strategies for various auth providers, and the Express.js security best practices.

Thanks for reading, and happy coding!

Similar Posts