A guide for getting up and running with a JWT user authentication system for a React application using an Express + Mongo backend.
-
npm i mongoose dotenv passport passport-jwt bcryptjs fastest-validator -
Connect to MongoDB using
mongoose
npm i axios jwt-decode react-router-dom
Add / Merge all files from the fileaddons folder into your project structure. The client folder is for the React install. The rest is for the Express application.
Other Notes
-
If you already have a
Usermodel, just make sure it has the 3 fields defined for the one provided here. -
If you have already implemented your own global store, you will need to merge the two actions,
LOGIN_USERandLOGOUT_USER, into your implementation. You will also then need to make sure that thedispatchcalls inutils/auth.jsmatch how your store is set up. -
If you already have an
api.jsfile, you will need to merge any current methods you have set up intoAPIclass provided inutils/api.js. Functionality inutils/auth.jsrequires this specificAPIclass to safely assign header information.
The first part of this guide walks through needed additions and modifications to your server application. These steps require the fileaddons from the previous section to be merged into your project.
-
Create the .env file at the root of your project
-
Make sure your .gitignore file has a rule to ignore this file. This file should NEVER be committed!
-
Add a new variable for
JWT_SECRET.
JWT_SECRET="A random string, which is used to help generate unique keys. This can be anything you want, quotes, a short passage from a book, random letters." - Open your
server.jsand add the following lines of code.
Import configuration from the .env file
Add at the very top of the file.
require("dotenv").config(); Import and configure passport passport helps us implement the authentication strategy. Add this somewhere near the top of the file.
const passport = require("passport"); app.use(passport.initialize()); // Passport config passport.use( require("./config/jwtPassportStrategy") ); Add API routes for authentication
Add before the catch all route to serve the React index.html (This one HTML route needed for React to work on Heroku)
app.use( "/api", require("./routes/authentication") ); These steps require the fileaddons/client/src to be merged into your React application.
Note: If you have already implemented your own global store functionality you can skip this step.
We want everything to have access to the store, including App so we are going to import into index.js and wrap it around everything.
- Inside
src/index.jsadd/modify the initial JSX template to include theStoreProvider.
import { StoreProvider } from "./store"; ReactDOM.render( <React.StrictMode> <StoreProvider> <App /> </StoreProvider> </React.StrictMode>, document.getElementById('root') ); Including the useAuthTokenStore hook will allow your app to reauthenticate already logged in users if they refresh the page, or leave and return to the application.
Example Usage
The best place for this is inside your primary App component.
// Import the useAuthTokenStore hook. import { useAuthTokenStore } from "./utils/auth"; function App() { // Use the hook to reauthenticate stored tokens. useAuthTokenStore(); /** Rest of your App component code here */ } The next 3 sections detail using the useLogin and useLogout hooks that help simplify the steps around successfully logging a user in and out of the application while keeping the React application's state and API requests in sync.
The provided useLogin hook provides a function that assists with:
-
Making the API request for logging in,
-
Storing the JWT token for reauthentication and applying it to the api class for authenticated API requests.
-
Pushing authenticated user information into the global store.
Example Usage
Below is a simple login form component that implements the useLogin hook.
function LoginForm() { const emailRef = useRef(); const passwordRef = useRef(); // Get the helper login function from the `useLogin` hook. const login = useLogin(); const handleSubmit = async e => { e.preventDefault(); const email = emailRef.current.value; const password = passwordRef.current.value; try { await login({ email, password }); // User has been successfully logged in and added to state. Perform any additional actions you need here such as redirecting to a new page. } catch(err) { // Handle error responses from the API if( err.response && err.response.data ) console.log(err.response.data); } } return ( <form onSubmit={handleSubmit}> <h2>Login</h2> <input type="text" ref={emailRef} placeholder="Your email" /><br /> <input type="password" ref={passwordRef} placeholder="Your password" /><br /> <button>Submit</button> </form> ) } The provided useLogout hook provides a function that assists with:
-
Clearing any stored authentication information from the application to log them out and remove their user from state.
-
Redirect them back to the home page.
Example Usage
Below is a simple button component that implements the useLogout hook.
function LogoutButton() { const logout = useLogout(); return <button onClick={logout}>Logout</button> } Registering a user requires the api.register method to be called with at least an email and password provided. Registering a new user does not automatically log them in, but the same login functionality above could be used to log a use in after successful registration.
Example Usage
Below is a simple registration form component that implements the api.register method and useLogin hook.
function RegistrationForm() { const emailRef = useRef(); const passwordRef = useRef(); // Get the helper login function from the `useLogin` hook. const login = useLogin(); const handleSubmit = async e => { e.preventDefault(); const email = emailRef.current.value; const password = passwordRef.current.value; try { // Register the user. await api.register({ email, password }); // User has been successfully registered, now log them in with the same information. await login({ email, password }); // User has been successfully registered, logged in and added to state. Perform any additional actions you need here such as redirecting to a new page. } catch(err) { // Handle error responses from the API. This will include if( err.response && err.response.data ) console.log(err.response.data); } } return ( <form onSubmit={handleSubmit}> <h2>Register</h2> <input type="text" ref={emailRef} placeholder="Your email" /><br /> <input type="password" ref={passwordRef} placeholder="Your password" /><br /> <button>Submit</button> </form> ) } Included in the React code are two custom components, PrivateRoute and GuestRoute that help combine Route from react-router-dom and the useIsAuthenticated hook from the provided auth.js to create automated redirects based on the current user's authentication status.
-
What is a Private Route? A private route is a route that can only be viewed by a user who IS actively logged into your application.
-
What is a Guest Route? A guest route is a route that can only be viewed by a user who IS NOT actively logged into your application.
The provided PrivateRoute component extends the basic Route component from react-router-dom by adding a layer of logic to redirect users to a more appropriate address in the event they have NOT logged into the system.
The PrivateRoute compontent will redirect users to / by default, but you can customize where they get sent with the redirectTo property.
Example Usage
Use the PrivateRoute component to hide the /members route from guest users. Guest users who try to go to /members will instead be sent to /login.
<PrivateRoute exact path="/members" redirectTo="/login" component={Members} /> The provided GuestRoute component extends the basic Route component from react-router-dom by adding a layer of logic to redirect users to a more appropriate address in the event they have already logged into the system.
The GuestRoute compontent will redirect users to / by default, but you can customize where they go with the redirectTo property.
Example Usage
Use the GuestRoute component to hide the /register route from logged in users. Logged in users who try to go to /register will instead be sent to /members.
<GuestRoute exact path="/register" redirectTo="/members" component={Register} /> {/* Routes open to all users */} <Route path="/" exact component={HomePage} /> <Route path="/about" exact component={AboutPage} /> <Route path="/privacy" exact component={PrivacyPolicyPage} /> {/* Routes for guest (non authenticated) users */} <GuestRoute exact path="/login" redirectTo="/members" component={LoginPage} /> <GuestRoute exact path="/register" redirectTo="/members" component={RegisterPage} /> {/* Routes for (authenticated) users */} <PrivateRoute exact path="/members" redirectTo="/login" component={MembersPage} /> Included in the provided auth.js utilities for React are two custom hooks that can be used in any component to create conditional templates based on the user's state.
-
useIsAuthenticated- Returnstrueorfalsebased on the user's current state of authentication. -
useAuthenticatedUser- Returns theuserdocument object for the currently authenticated user orundefinedif not available.
The useIsAuthenticated hook is perfect for situations where you need to change what's displayed base on whether users are logged in or out.
Example Usage
Only render the /register and /login links for guest users and only render the LogoutButton for logged in users.
function MyNavBar() { const isAuthenticated = useIsAuthenticated(); return ( <div className="navbar"> <Link to="/">Home</Link> {!isAuthenticated && <Link to="/register">Register</Link>} {!isAuthenticated && <Link to="/login">Login</Link>} {isAuthenticated && <LogoutButton>} </div> ); } The useAuthenticatedUser hook is what you'll use anywhere you need specific information about the current user.
Example Usage
Display the authenticated user's email when one exists.
function Profile() { const user = useAuthenticatedUser(); return user && ( <div> <h2>My Profile</h2> <p> <strong>Email:</strong> {user.email} </p> </div> ); }