In this tutorial, we’re gonna build a React.js Login & Registration example with JWT & HttpOnly Cookie, React Router, Axios and Bootstrap (without Redux). I will show you:
- Flow for User Signup, Login, Logout with JWT & HttpOnly Cookie
- Project Structure for React Hooks JWT Login example (without Redux) with LocalStorage, React Router & Axios
- Creating Reactjs Function Components with Hooks & Form Validation
- React Function Components for accessing protected Resources (Authorization)
- Dynamic Navigation Bar in React Hooks App
Related Posts:
– Using Redux: React Redux Toolkit Authentication & Authorization example
– In-depth Introduction to JWT-JSON Web Token
– React Hooks CRUD example with Axios and Web API
– React Hooks File Upload example with Axios & Progress Bar
– React Form Validation with Hooks example
– React Custom Hook
Fullstack (JWT Authentication & Authorization example):
– React + Spring Boot
– React + Node.js Express + MySQL/PostgreSQL
– React + Node.js Express + MongoDB
Contents
- Overview
- Flow
- Diagram
- Technology
- Project Structure
- Setup React.js Project
- Add React Router
- Import Bootstrap
- Setup Proxy
- Create Services
- Create React Pages for Authentication
- Create React Pages for accessing Resources
- Add Navbar and define Routes
- Logout when Token is expired
- Add CSS style for React Pages
- Run the Project
- Conclusion
- Further Reading
Overview of React.js Login and Registration example
We will build a React Hooks application with Login, Logout and Registration using JWT and HttpOnly Cookie in that:
- There are Login/Logout, Signup pages.
- Form data will be validated by front-end before being sent to back-end.
- Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.
Here are the screenshots:
– Signup Page:
– React Login and Registration Form Validation:
If you need Form Validation with React Hook Form 7, please visit:
React Form Validation with Hooks example
Or Formik and Yup:
React Form Validation example with Hooks, Formik and Yup
– Login Page:
HttpOnly Cookie set by the Server:
– Profile Page (for successful Login):
– For Authorization (Moderator account login), the navigation bar will change by authorities:
HttpOnly Cookie sent with HTTP Request:
– Browser Local Storage for storing user information:
If you want to add refresh token, please visit:
React Refresh Token with JWT and Axios Interceptors
User Authentication and Authorization Flow
For JWT Authentication, we’re gonna call 3 endpoints:
- POST
api/auth/signup
for User Registration - POST
api/auth/signin
for User Login - POST
api/auth/signout
for User Logout
The following flow shows you an overview of Requests and Responses that React Client will make or receive. This React Client uses JWT in Cookies while sending request to protected resources.
You can find step by step to implement these back-end servers in following tutorial:
- Spring Boot Login and Registration example with H2
- Spring Boot Login and Registration example with MySQL
- Spring Boot Login and Registration example with MongoDB
- Node.js Express Login and Registration example with MySQL
- Node.js Express Login and Registration example with MongoDB
Demo Video
This is full React + Spring Boot JWT Authentication & Authorization demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User):
React + Node Express:
The React project in video uses React Components instead of React Hooks, and Local Storage instead of HttpOnly Cookie for storing JWT, but the flow or structure is the same.
React Function Component Diagram with Router, Axios & LocalStorage
Let’s look at the diagram below.
– The App
component is a container with React Router (BrowserRouter
). Basing on the state, the navbar can display its items.
– Login
& Register
pages have form for data submission (with support of react-validation
library). They call methods from auth.service
to make login/register request.
– auth.service
methods use axios
to make HTTP requests. It also stores or gets User Profile from Browser Local Storage inside these methods.
– Home
page is public for all visitor.
– Profile
page displays user information after the login action is successful.
– BoardUser
, BoardModerator
, BoardAdmin
pages will be displayed by state user.roles
. In these pages, we use user.service
to access protected resources from Web API.
– user.service
methods use axios
to make Authorization requests for accessing protected resources (with JWT in HttpOnly Cookie).
If you want to use Typescript instead, please visit:
React Typescript Authentication example with Hooks
Technology
We’re gonna use these modules:
- React 17
- react-router-dom 6.2.1
- axios 0.24.0
- react-validation 3.0.7
- Bootstrap 4
- validator 13.7.0
- http-proxy-middleware 2
Project Structure
This is folders & files structure for this React application:
With the explanation in diagram above, you can understand the project structure easily.
For common folder, there are two ways to handle JWT Token expiration.
For more details, please visit:
Handle JWT Token expiration in React with Hooks
Setup React Login and Registration Project
Open cmd at the folder you want to save Project folder, run command:
npx create-react-app react-hooks-login-registration
Add React Router
– Run the command:
npm install react-router-dom
Or: yarn add react-router-dom
– Open src/index.js and wrap App
component by BrowserRouter
object.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from "react-router-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
reportWebVitals();
Import Bootstrap
Run command:
npm install [email protected]
Or: yarn add [email protected]
Open src/App.js and modify the code inside it as following-
import React from "react";
import "bootstrap/dist/css/bootstrap.min.css";
function App() {
return (
...
);
}
export default App;
Setup Proxy
The HttpOnly Cookie approach in this tutorial works if the React app and the back-end server hosted in same domain. So we need to use http-proxy-middleware for local development.
Run command:
npm install http-proxy-middleware
Or: yarn add http-proxy-middleware
In the src folder, create setupProxy.js file with following code:
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function (app) {
app.use(
"/api",
createProxyMiddleware({
target: "http://localhost:8080",
changeOrigin: true,
})
);
};
Create Services
We’re gonna create two services in src/services folder:
- Authentication service
- Data service
services
auth.service.js (Authentication service)
user.service.js (Data service)
Before working with these services, we need to install Axios with command:
npm install axios
Or: yarn add axios
Authentication service
The service uses Axios for HTTP requests and Local Storage for user information & JWT.
It provides following important functions:
login()
: POST {username, password} & saveUser Profile
to Local Storagelogout()
: POST logout request, removeUser Profile
from Local Storageregister()
: POST {username, email, password}getCurrentUser()
: get stored user information
services/auth.service.js
import axios from "axios";
const API_URL = "http://localhost:3000/api/auth/";
const register = (username, email, password) => {
return axios.post(API_URL + "signup", {
username,
email,
password,
});
};
const login = (username, password) => {
return axios
.post(API_URL + "signin", {
username,
password,
})
.then((response) => {
if (response.data.username) {
localStorage.setItem("user", JSON.stringify(response.data));
}
return response.data;
});
};
const logout = () => {
localStorage.removeItem("user");
return axios.post(API_URL + "signout").then((response) => {
return response.data;
});
};
const getCurrentUser = () => {
return JSON.parse(localStorage.getItem("user"));
};
const AuthService = {
register,
login,
logout,
getCurrentUser,
}
export default AuthService;
Data service
We also have methods for retrieving data from server, in the case we access protected resources. Because HttpOnly Cookies will be automatically sent along with HTTP requests, so we just simply use Axios without caring JWT.
services/user.service.js
import axios from "axios";
const API_URL = "http://localhost:3000/api/test/";
const getPublicContent = () => {
return axios.get(API_URL + "all");
};
const getUserBoard = () => {
return axios.get(API_URL + "user");
};
const getModeratorBoard = () => {
return axios.get(API_URL + "mod");
};
const getAdminBoard = () => {
return axios.get(API_URL + "admin");
};
const UserService = {
getPublicContent,
getUserBoard,
getModeratorBoard,
getAdminBoard,
}
export default UserService;
Create React Pages for Authentication
In src folder, create new folder named components and add several files as following:
components
Login.js
Register.js
Profile.js
Form Validation overview
Now we need a library for Form validation, so we’re gonna add react-validation library to our project.
Run the command: npm install react-validation validator
To use react-validation in this example, you need to import following items:
import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";
import { isEmail } from "validator";
We also use isEmail()
function from validator to verify email.
This is how we put them in render()
method with validations
attribute:
const required = value => {
if (!value) {
return (
<div className="invalid-feedback d-block">
This field is required!
</div>
);
}
};
const email = value => {
if (!isEmail(value)) {
return (
<div className="invalid-feedback d-block">
This is not a valid email.
</div>
);
}
};
render() {
return (
...
<Form
onSubmit={handleLogin}
ref={form}
>
...
<Input
type="text"
className="form-control"
...
validations={[required, email]}
/>
<CheckButton
style={{ display: "none" }}
ref={checkBtn}
/>
</Form>
...
);
}
We’re gonna call Form validateAll()
method to check validation functions in validations
. Then CheckButton
helps us to verify if the form validation is successful or not. So this button will not display on the form.
form.validateAll();
if (checkBtn.context._errors.length === 0) {
// do something when no error
}
If you need Form Validation with React Hook Form 7, please visit:
React Form Validation with Hooks example
Or Formik and Yup:
React Form Validation example with Hooks, Formik and Yup
Login Page
This page has a Form with username
& password
.
– We’re gonna verify them as required field.
– If the verification is ok, we call AuthService.login()
method, then direct user to Profile page: props.history.push("/profile");
, or show message
with response error.
components/Login.js
import React, { useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";
import AuthService from "../services/auth.service";
const required = (value) => {
if (!value) {
return (
<div className="invalid-feedback d-block">
This field is required!
</div>
);
}
};
const Login = () => {
const form = useRef();
const checkBtn = useRef();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
const navigate = useNavigate();
const onChangeUsername = (e) => {
const username = e.target.value;
setUsername(username);
};
const onChangePassword = (e) => {
const password = e.target.value;
setPassword(password);
};
const handleLogin = (e) => {
e.preventDefault();
setMessage("");
setLoading(true);
form.current.validateAll();
if (checkBtn.current.context._errors.length === 0) {
AuthService.login(username, password).then(
() => {
navigate("/profile");
window.location.reload();
},
(error) => {
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setLoading(false);
setMessage(resMessage);
}
);
} else {
setLoading(false);
}
};
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleLogin} ref={form}>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block" disabled={loading}>
{loading && (
<span className="spinner-border spinner-border-sm"></span>
)}
<span>Login</span>
</button>
</div>
{message && (
<div className="form-group">
<div className="alert alert-danger" role="alert">
{message}
</div>
</div>
)}
<CheckButton style={{ display: "none" }} ref={checkBtn} />
</Form>
</div>
</div>
);
};
export default Login;
Register Page
This page is similar to Login Page.
For Form Validation, there are some more details:
username
: required, between 3 and 20 charactersemail
: required, email formatpassword
: required, between 6 and 40 characters
We’re gonna call AuthService.register()
method and show response message (successful or error).
components/Register.js
import React, { useState, useRef } from "react";
import Form from "react-validation/build/form";
import Input from "react-validation/build/input";
import CheckButton from "react-validation/build/button";
import { isEmail } from "validator";
import AuthService from "../services/auth.service";
const required = (value) => {
if (!value) {
return (
<div className="invalid-feedback d-block">
This field is required!
</div>
);
}
};
const validEmail = (value) => {
if (!isEmail(value)) {
return (
<div className="invalid-feedback d-block">
This is not a valid email.
</div>
);
}
};
const vusername = (value) => {
if (value.length < 3 || value.length > 20) {
return (
<div className="invalid-feedback d-block">
The username must be between 3 and 20 characters.
</div>
);
}
};
const vpassword = (value) => {
if (value.length < 6 || value.length > 40) {
return (
<div className="invalid-feedback d-block">
The password must be between 6 and 40 characters.
</div>
);
}
};
const Register = (props) => {
const form = useRef();
const checkBtn = useRef();
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [successful, setSuccessful] = useState(false);
const [message, setMessage] = useState("");
const onChangeUsername = (e) => {
const username = e.target.value;
setUsername(username);
};
const onChangeEmail = (e) => {
const email = e.target.value;
setEmail(email);
};
const onChangePassword = (e) => {
const password = e.target.value;
setPassword(password);
};
const handleRegister = (e) => {
e.preventDefault();
setMessage("");
setSuccessful(false);
form.current.validateAll();
if (checkBtn.current.context._errors.length === 0) {
AuthService.register(username, email, password).then(
(response) => {
setMessage(response.data.message);
setSuccessful(true);
},
(error) => {
const resMessage =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setMessage(resMessage);
setSuccessful(false);
}
);
}
};
return (
<div className="col-md-12">
<div className="card card-container">
<img
src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
alt="profile-img"
className="profile-img-card"
/>
<Form onSubmit={handleRegister} ref={form}>
{!successful && (
<div>
<div className="form-group">
<label htmlFor="username">Username</label>
<Input
type="text"
className="form-control"
name="username"
value={username}
onChange={onChangeUsername}
validations={[required, vusername]}
/>
</div>
<div className="form-group">
<label htmlFor="email">Email</label>
<Input
type="text"
className="form-control"
name="email"
value={email}
onChange={onChangeEmail}
validations={[required, validEmail]}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password</label>
<Input
type="password"
className="form-control"
name="password"
value={password}
onChange={onChangePassword}
validations={[required, vpassword]}
/>
</div>
<div className="form-group">
<button className="btn btn-primary btn-block">Sign Up</button>
</div>
</div>
)}
{message && (
<div className="form-group">
<div
className={
successful ? "alert alert-success" : "alert alert-danger"
}
role="alert"
>
{message}
</div>
</div>
)}
<CheckButton style={{ display: "none" }} ref={checkBtn} />
</Form>
</div>
</div>
);
};
export default Register;
Profile Page
This page gets current User from Local Storage by calling AuthService.getCurrentUser()
method and show user information.
components/Profile.js
import React from "react";
import AuthService from "../services/auth.service";
const Profile = () => {
const currentUser = AuthService.getCurrentUser();
return (
<div className="container">
<header className="jumbotron">
<h3>
<strong>{currentUser.username}</strong> Profile
</h3>
</header>
<p>
<strong>Id:</strong> {currentUser.id}
</p>
<p>
<strong>Email:</strong> {currentUser.email}
</p>
<strong>Authorities:</strong>
<ul>
{currentUser.roles &&
currentUser.roles.map((role, index) => <li key={index}>{role}</li>)}
</ul>
</div>
);
};
export default Profile;
Create React Pages for accessing Resources
These pages will use UserService
to request data from API.
components
Home.js
BoardUser.js
BoardModerator.js
BoardAdmin.js
Home Page
This is a public page that shows public content. People don’t need to log in to view this page.
components/Home.js
import React, { useState, useEffect } from "react";
import UserService from "../services/user.service";
const Home = () => {
const [content, setContent] = useState("");
useEffect(() => {
UserService.getPublicContent().then(
(response) => {
setContent(response.data);
},
(error) => {
const _content =
(error.response && error.response.data) ||
error.message ||
error.toString();
setContent(_content);
}
);
}, []);
return (
<div className="container">
<header className="jumbotron">
<h3>{content}</h3>
</header>
</div>
);
};
export default Home;
Role-based Pages
We’re gonna have 3 pages for accessing protected data:
- BoardUser page calls
UserService.getUserBoard()
- BoardModerator page calls
UserService.getModeratorBoard()
- BoardAdmin page calls
UserService.getAdminBoard()
I will show you User Page for example, other Pages are similar to this Page.
components/BoardUser.js
import React, { useState, useEffect } from "react";
import UserService from "../services/user.service";
const BoardUser = () => {
const [content, setContent] = useState("");
useEffect(() => {
UserService.getUserBoard().then(
(response) => {
setContent(response.data);
},
(error) => {
const _content =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
setContent(_content);
}
);
}, []);
return (
<div className="container">
<header className="jumbotron">
<h3>{content}</h3>
</header>
</div>
);
};
export default BoardUser;
You can simplify import statement with:
Absolute Import in React
Logout when Token is expired
There are two ways to handle JWT Token expiration.
For more details, please visit:
Handle JWT Token expiration in React with Hooks
Now we add a navigation bar in App
component. This is the root container for our application.
The navbar dynamically changes by login status and current User’s roles.
- Home: always
- Login & Sign Up: if user hasn’t signed in yet
- User:
AuthService.getCurrentUser()
returns a value - Board Moderator: roles includes
ROLE_MODERATOR
- Board Admin: roles includes
ROLE_ADMIN
App.js
import React, { useState, useEffect } from "react";
import { Routes, Route, Link } from "react-router-dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "./App.css";
import AuthService from "./services/auth.service";
import Login from "./components/Login";
import Register from "./components/Register";
import Home from "./components/Home";
import Profile from "./components/Profile";
import BoardUser from "./components/BoardUser";
import BoardModerator from "./components/BoardModerator";
import BoardAdmin from "./components/BoardAdmin";
// import AuthVerify from "./common/AuthVerify";
import EventBus from "./common/EventBus";
const App = () => {
const [showModeratorBoard, setShowModeratorBoard] = useState(false);
const [showAdminBoard, setShowAdminBoard] = useState(false);
const [currentUser, setCurrentUser] = useState(undefined);
useEffect(() => {
const user = AuthService.getCurrentUser();
if (user) {
setCurrentUser(user);
setShowModeratorBoard(user.roles.includes("ROLE_MODERATOR"));
setShowAdminBoard(user.roles.includes("ROLE_ADMIN"));
}
EventBus.on("logout", () => {
logOut();
});
return () => {
EventBus.remove("logout");
};
}, []);
const logOut = () => {
AuthService.logout();
setShowModeratorBoard(false);
setShowAdminBoard(false);
setCurrentUser(undefined);
};
return (
<div>
<nav className="navbar navbar-expand navbar-dark bg-dark">
<Link to={"/"} className="navbar-brand">
bezKoder
</Link>
<div className="navbar-nav mr-auto">
<li className="nav-item">
<Link to={"/home"} className="nav-link">
Home
</Link>
</li>
{showModeratorBoard && (
<li className="nav-item">
<Link to={"/mod"} className="nav-link">
Moderator Board
</Link>
</li>
)}
{showAdminBoard && (
<li className="nav-item">
<Link to={"/admin"} className="nav-link">
Admin Board
</Link>
</li>
)}
{currentUser && (
<li className="nav-item">
<Link to={"/user"} className="nav-link">
User
</Link>
</li>
)}
</div>
{currentUser ? (
<div className="navbar-nav ml-auto">
<li className="nav-item">
<Link to={"/profile"} className="nav-link">
{currentUser.username}
</Link>
</li>
<li className="nav-item">
<a href="/login" className="nav-link" onClick={logOut}>
LogOut
</a>
</li>
</div>
) : (
<div className="navbar-nav ml-auto">
<li className="nav-item">
<Link to={"/login"} className="nav-link">
Login
</Link>
</li>
<li className="nav-item">
<Link to={"/register"} className="nav-link">
Sign Up
</Link>
</li>
</div>
)}
</nav>
<div className="container mt-3">
<Routes>
<Route exact path={"/"} element={<Home />} />
<Route exact path={"/home"} element={<Home />} />
<Route exact path="/login" element={<Login />} />
<Route exact path="/register" element={<Register />} />
<Route exact path="/profile" element={<Profile />} />
<Route path="/user" element={<BoardUser />} />
<Route path="/mod" element={<BoardModerator />} />
<Route path="/admin" element={<BoardAdmin />} />
</Routes>
</div>
{/* <AuthVerify logOut={logOut}/> */}
</div>
);
};
export default App;
Add CSS style for React Pages
Open src/App.css and write some CSS code as following:
label {
display: block;
margin-top: 10px;
}
.card-container.card {
max-width: 350px !important;
padding: 40px 40px;
}
.card {
background-color: #f7f7f7;
padding: 20px 25px 30px;
margin: 0 auto 25px;
margin-top: 50px;
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
-webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}
.profile-img-card {
width: 96px;
height: 96px;
margin: 0 auto 10px;
display: block;
-moz-border-radius: 50%;
-webkit-border-radius: 50%;
border-radius: 50%;
}
Run the Project
You need to run one of following Rest API servers first:
- Spring Boot Login and Registration example with H2
- Spring Boot Login and Registration example with MySQL
- Spring Boot Login and Registration example with MongoDB
- Node.js Express Login and Registration example with MySQL
- Node.js Express Login and Registration example with MongoDB
Then run this React Auth Application with command:
npm run start
Or: yarn start
Conclusion
Congratulation!
Today we’ve done so many interesting things. I hope you understand the overall layers of our React Login and Registraion App (without Redux) using Hooks, HttpOnly Cookie, LocalStorage, React Router, Axios, Bootstrap. Now you can apply it in your project at ease.
Don’t forget to read this tutorial:
Handle JWT Token expiration in React with Hooks
Or you need React Redux for this example:
React Redux Toolkit Authentication & Authorization example
Or add refresh token:
React Refresh Token with JWT and Axios Interceptors
Happy learning, see you again!
Further Reading
- React Router Guide
- React Hooks
- React Custom Hook
- https://www.npmjs.com/package/react-validation
- https://www.npmjs.com/package/validator
- In-depth Introduction to JWT-JSON Web Token
Fullstack CRUD:
– React Redux + Spring Boot
– React + Spring Boot + MySQL
– React + Spring Boot + PostgreSQL
– React + Spring Boot + MongoDB
– React + Node.js Express + MySQL
– React + Node.js Express + PostgreSQL
– React + Node.js Express + MongoDB
– React + Django
Source Code
You can find the complete source code for this tutorial on Github.
Typescript version:
React Typescript Authentication example with Hooks
Using Redux:
React Redux Toolkit Authentication & Authorization example
Excellent, amazing tutorial, thanks for sharing!