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
- The user submits their login credentials (e.g. username/password) via a form in the React UI
- Those credentials are sent to the backend API, usually via a POST request to a
/login
route - The server passes the credentials to the Passport middleware
- Passport uses a strategy (e.g.
LocalStrategy
) to look up the user in a database or other user store - If the credentials are valid, Passport creates a user session and sends back a session cookie
- On subsequent requests, the session cookie is included, keeping the user logged in
- 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 ourLocalStrategy
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 infopages/login.js
– the React component with our login formpages/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!