How to Authenticate Your React App with Passport.js

Authentication is a critical part of most web applications. It allows you to provide a personalized experience for your users and protect sensitive data or actions. However, implementing authentication from scratch is complex and error-prone. Security holes can lead to compromised user data in your app.

Fortunately, you don‘t have to reinvent the wheel thanks to battle-tested open-source libraries like Passport.js. With over 1.8 million weekly downloads and 320k+ dependent repositories, Passport has become the go-to solution for handling authentication in Node.js and React projects.

Why Use Passport.js for Authentication?

Passport provides several key advantages over implementing auth yourself or using a less mature library:

  • Modular architecture supporting 500+ login strategies
  • Extensive community support and documentation with 20k+ GitHub stars
  • Widely adopted by top companies like IBM and Okta
  • Stays current with security best practices and latest OAuth protocols

Passport‘s strategy-based architecture makes it flexible enough to handle everything from basic username/password login to complex OAuth flows with social providers like Google, Facebook, and Twitter. And best of all, Passport is 100% open source and free to use, so you‘re not locked into any pricing plans or vendor restrictions.

Authentication Flow with Passport and React

At a high level, here‘s how authentication works in a React app using Passport:

graph LR
A[User] --> B[React UI]
B --> C{API}
C --> D{Passport}
D --> E[Strategy]
E --> F{User Store}
F --> G[Session]
G --> A
  1. The user submits their login credentials (e.g. username/password) via a form in the React UI
  2. Those credentials are sent to the backend API, usually via a POST request to a /login route
  3. The server passes the credentials to the Passport middleware
  4. Passport uses a strategy (e.g. LocalStrategy) to look up the user in a database or other user store
  5. If the credentials are valid, Passport creates a user session and sends back a session cookie
  6. On subsequent requests, the session cookie is included, keeping the user logged in
  7. To log out, the React app sends a request to a /logout API route to destroy the session

Passport slots into this flow as a middleware layer that handles the interaction with the user store and creation of the session. Let‘s break down some of the key concepts.

Strategies

Passport uses the concept of strategies to support various ways of authenticating. A strategy is a module that verifies user credentials and supplies the verified user data to Passport. The LocalStrategy is used for username/password login, while other strategies handle OAuth flows for social logins.

import LocalStrategy from ‘passport-local‘

passport.use(new LocalStrategy(
  function (username, password, done) {
    User.findOne({ username }, (err, user) => {
      if (err) return done(err)
      if (!user) return done(null, false)

      user.verifyPassword(password, (err, isMatch) => {  
        if (err) return done(err)
        if (!isMatch) return done(null, false)

        return done(null, user)
      })
    })
  }
))

Sessions

Once a user is authenticated, Passport creates a session to persist their logged in state. This is typically done by saving the user ID in an HTTP-only session cookie. On subsequent requests, Passport reads the session cookie and deserializes it back into the full user object.

passport.serializeUser((user, done) => {
  done(null, user.id)
})

passport.deserializeUser((id, done) => {  
  User.findById(id, (err, user) => {
    done(err, user)
  })
})

Setting Up Passport.js in a React Project

To demonstrate a complete Passport authentication flow, we‘ll use the popular Next.js framework for building React applications. Next.js provides a backend API layer which makes it easier to integrate with Passport. However, the same principles apply to other React setups like Create React App.

For brevity, we‘ll assume you already have a Next.js project set up. If you‘re starting from scratch, follow the official docs to bootstrap a new project.

Install dependencies

First, install the necessary packages:

npm install passport passport-local bcryptjs next-connect next-session 

This includes the core passport library, the LocalStrategy, and some helpful utilities for handling password hashing, routing, and sessions.

Configure Passport

Next, we need to set up Passport and tell it how to serialize/deserialize user data to/from the session. Create a new file lib/passport.js with:

import passport from "passport"
import LocalStrategy from "passport-local"
import { findUserByUsername, validatePassword } from "./user"

passport.serializeUser((user, done) => {
  done(null, user.username)
})

passport.deserializeUser(async (username, done) => {
  const user = await findUserByUsername(username)
  done(null, user)
})

passport.use(new LocalStrategy(async (username, password, done) => {
  const user = await findUserByUsername(username)
  if (!user) return done(null, false)

  const isValid = await validatePassword(user, password)
  if (!isValid) return done(null, false)

  return done(null, user)
}))

export default passport

This module exports a configured Passport instance with a LocalStrategy that looks up users by username and validates their password. The serializeUser and deserializeUser methods tell Passport how to store and retrieve the user from the session.

Set up API routes

With Passport configured, we can set up the API routes that the React frontend will use to login and logout. These routes go in the pages/api directory which Next.js uses for its built-in API routes.

pages/api/login.js

import passport from "lib/passport"
import nextConnect from "next-connect"
import { setLoginSession } from "lib/session"

const authenticate = (method, req, res) => 
  new Promise((resolve, reject) => {
    passport.authenticate(method, { session: false }, (err, user) => {
      if (err) reject(err)
      else resolve(user)
    })(req, res)
  })

export default nextConnect()
  .use(passport.initialize())
  .post(async (req, res) => {
    try {
      const user = await authenticate(‘local‘, req, res)
      const session = { passport: { user: user.username }}

      await setLoginSession(res, session)
      res.status(200).send({ done: true })
    } catch (err) {
      console.error(err)
      res.status(401).send(err.message)
    }
  })

pages/api/logout.js

import { removeTokenCookie } from "lib/session"

export default async (req, res) => {
  removeTokenCookie(res)
  res.redirect(‘/‘)
}

The /login route initializes Passport and passes the request to the LocalStrategy we defined earlier. If authentication succeeds, it creates a login session with the authenticated user.

The /logout route simply removes the session token cookie to log the user out and redirects to the homepage.

Access user from React

With the backend routes in place, the final step is to call them from our React components and access the logged in user. We can create a helper hook to fetch the current user:

import { useEffect } from "react"
import Router from "next/router"
import useSWR from "swr"

const fetcher = url => fetch(url).then(r => r.json())

export function useUser({ redirectTo, redirectIfFound } = {}) {
  const { data, error } = useSWR(‘/api/user‘, fetcher)
  const isLoading = !error && !data
  const user = data?.user

  useEffect(() => {
    if (!redirectTo || isLoading) return
    if (redirectTo && !redirectIfFound && !user) Router.push(redirectTo)
    if (redirectIfFound && user) Router.push(redirectTo)
  }, [user, redirectTo, redirectIfFound, isLoading])

  return error ? null : user
}

This useUser hook generates a request to a /api/user route to fetch the current user, and optionally redirects based on the auth state. The /api/user route simply reads the logged in user from the session cookie:

import { getLoginSession } from "lib/session"

export default async (req, res) => {
  try {
    const session = await getLoginSession(req)
    const user = session?.passport?.user

    res.status(200).json({ user })
  } catch (error) {
    console.error(error)
    res.status(500).send(error.message)
  }
}

Example App

To solidify these concepts, let‘s walk through a complete example of adding Passport to a React app. The full code is available on GitHub.

First, we have a typical Next.js project structure:

.
├── components
│   ├── Layout.js
│   └── Nav.js
├── lib
│   ├── passport.js  
│   ├── session.js
│   └── user.js
├── pages  
│   ├── api
│   │   ├── login.js
│   │   ├── logout.js
│   │   └── user.js
│   ├── dashboard.js
│   ├── index.js
│   └── login.js
├── package.json
└── next.config.js

The key files to note are:

  • lib/passport.js – configures Passport with our LocalStrategy
  • lib/user.js – exports dummy user lookup/validation functions (swapped with DB queries in a real app)
  • pages/api/* – our API routes for login, logout, and user info
  • pages/login.js – the React component with our login form
  • pages/dashboard.js – a protected page, only accessible to logged in users

In pages/login.js, we have a basic form that sends a POST to our /api/login route when submitted:

import { useState } from "react"
import { useUser } from "lib/user"

const Login = () => {
  useUser({ redirectTo: "/dashboard", redirectIfFound: true })

  const [errorMsg, setErrorMsg] = useState("")

  async function handleSubmit(e) {
    e.preventDefault()

    const body = {
      username: e.currentTarget.username.value,
      password: e.currentTarget.password.value,
    }

    try {
      const res = await fetch("/api/login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      })
      if (res.status === 200) Router.push("/dashboard")
    } catch (error) {
      console.error("An unexpected error occurred:", error)
      setErrorMsg(error.message)
    }
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>
          <span>Username</span>
          <input type="text" name="username" required />
        </label>

        <label>
          <span>Password</span>
          <input type="password" name="password" required />
        </label>

        <button type="submit">Login</button>

        {errorMsg && <p>{errorMsg}</p>}
      </form>
    </div>
  )
}

export default Login

If login is successful, the browser will be redirected to the /dashboard route. The dashboard page utilizes our useUser hook to access the user data and display a greeting:

import { useUser } from "lib/user"

const Dashboard = () => {
  const user = useUser({ redirectTo: "/login" })

  if (!user) return <div>Loading...</div>

  return (
    <>

      <h2>Welcome, {user.username}!</h2>
      <a href="/api/logout">Logout</a>
    </>
  )
}

export default Dashboard

Clicking the logout link will hit the /logout API route to clear the session and redirect to the homepage.

With that, we have a fully functioning auth flow in our React app! While this example uses some dummy data, you can plug in your own database of users and adapt it to your needs.

Best Practices for Production Apps

While Passport and Next.js make it easy to add authentication to a React app, there are some additional considerations to keep in mind for production deployments:

  • Use HTTPS – Authentication should always be done over a secure connection in production. You can use a service like Let‘s Encrypt to obtain free SSL certificates.

  • Use a production-grade session store – The default in-memory session store is not suitable for production use. Instead, use a scalable store like Redis or MySQL.

  • Don‘t store credentials in code – Sensitive config like API keys and database passwords should be kept in environment variables to avoid accidentally exposing them in version control.

  • Implement proper access control – Authentication is only half the picture. Make sure to also set up proper authorization rules to restrict access to certain routes/data based on user roles.

By following these best practices, you can deploy your Passport-enabled React app to production with confidence! I encourage you to check out the Passport docs to learn more about its many features and customization options.

Conclusion

In summary, Passport.js is a powerful and flexible auth library that integrates seamlessly with React apps. Its modular design and wide selection of strategies make it suitable for a variety of use cases. And by leveraging frameworks like Next.js, we can add sophisticated authentication flows to our apps with minimal boilerplate.

I hope this in-depth tutorial has given you the knowledge and confidence to add Passport to your own projects. The complete example code is available on GitHub for you to reference and adapt.

Authentication can be a complex topic, but Passport abstracts many of the tricky implementation details. Combined with React‘s component model, you can create secure and scalable user experiences. So what are you waiting for? Go build something awesome!

If you have any questions or feedback, feel free to drop a comment below or reach out on Twitter. Happy coding!

Similar Posts