User Access Control in React
By Syncfusion®, Inc. · Last updated
Authentication and authorization with JWT: the login flow, user identity and roles, role-based access control, protected routes and guards, an auth context and auth service, login state, logout and best practices.
What Is Authentication?
Authentication means checking who a user is. The most common way is a login form: the user types an email and a password, and the server checks them.
If they are correct, the user is logged in. The app now knows who is using it.
Real-World Example
In a bookshop app, the user signs in with an email and a password. The app checks them and remembers who the user is.
A guest who opens the My orders page is sent to the sign-in page. A normal customer does not see the Admin page in the menu, because only a user with the admin role may open it.
Authentication answers one question: Who are you?
What Is Authorization?
Authorization means checking what a logged-in user is allowed to do. For example, a customer can see their own orders, but only an admin can edit the book list.
Authorization always comes after authentication. You must know who the user is before you can decide what they may do.
Authorization answers a different question: What are you allowed to do?
Authentication vs Authorization
Authentication asks who you are. Authorization asks what you are allowed to do
Authentication Who are you? Check the email and password → "You are Asha"
Authorization What can you do? Check the role → "Asha may see orders, not admin" Always in this order: first who, then what.
Authentication comes first. Authorization uses the result to decide what is allowed.
| Authentication | Authorization | |
|---|---|---|
| Question | Who are you? | What can you do? |
| When | First, at login | After login, on every protected action |
| Checks | Email and password (or Google, OTP, ...) | The user's role or permissions |
| If it fails | "Wrong email or password" | "You do not have access to this page" |
| Example | Asha logs in | Asha may see her orders, but not the admin page |
Authentication Flow Overview
Here is what happens when a user logs in to a React app that uses JWT.
A JWT (JSON Web Token, said "jot") is a long piece of text that the server gives the app after a correct login. The app sends it with each request, so the server knows who the user is without asking for the password again.
Login flow: the user sends email and password, the server checks them and returns a JWT, the app keeps it and sends it with each request
User types details
Login form onSubmit
sends
Auth server checks password
JWT
App keeps the token
AuthContext user = Asha
API token sent with requests
After login, the app keeps a token. It proves who the user is, without sending the password again.
- The user types an email and password in the login form.
- The app sends them to the server's login API.
- The server checks them. If they are correct, it creates a JWT and sends it back.
- The app keeps the token, and stores the user's details in state.
- For every later request, the app sends the token along. The server checks the token and answers.
What Is Inside a JWT?
A JWT has three parts, joined by dots.
A JWT has three parts separated by dots: header, payload and signature eyJhbGci.eyJzdWIiOiIxIi....SflKxwRJ
Header which algorithm
Payload sub, name, role, exp
Signature made with the server's secret
The payload holds the user's details. The signature proves the server made the token.
- Header — says how the token was signed.
- Payload — the user's details, called claims. For example:
sub(the user's id),name,role, andexp(when the token expires). - Signature — made by the server with a secret key. If anyone changes the payload, the signature no longer matches, and the server rejects the token.
The header and payload are only encoded, not hidden. Anyone with the token can read them. So never put a password or other secrets in a JWT.
User Identity
User identity is the information that says who the logged-in user is. In a JWT app, the identity comes from the token's payload.
The lab below has a pretend auth service. It acts like a server: it checks the password and creates a JWT-shaped token. Then the app reads the payload back to learn who logged in.
Click Log in as Asha. After half a second, the token appears: three parts joined by dots. Below it, the app shows the name and role it read from the payload. Click Wrong password, and you get an error instead of a token.
In a real app, the browser cannot create a real JWT, because it does not have the server's secret key. The pretend service on this page is only for learning. The signature here is fake on purpose.
User Roles
A role is a group name for users who may do the same things. Most apps have only a few roles, for example:
- customer — can view books and place orders.
- admin — can do everything a customer can, and also edit books and see all orders.
The role is usually part of the JWT payload, so the app knows it right after login: role: "admin".
Role-Based Access Control (RBAC)
Role-based access control (RBAC) means you give permissions to roles, not to each person. A user can do something if their role allows it.
Role-based access control: users get roles, and roles get permissions
Asha
Ravi
customer
admin
view books
place orders
edit books
see all orders
You check the role, not the person. Change a role's permissions once, and every user with that role is updated.
A simple way to write RBAC is a list of permissions for each role, and one function to check it.
const permissions = {
customer: ["view-books", "place-order"],
admin: ["view-books", "place-order", "edit-books", "see-all-orders"],
};
function can(role: Role, action: string): boolean {
return permissions[role].includes(action);
}
can("customer", "edit-books"); // false
can("admin", "edit-books"); // true
Now the app asks can(user.role, "edit-books") instead of checking names. If the shop adds a new "manager" role, you only add one line to the list.
Change the role. The list of allowed actions changes at once. The "manager" role was added with just one line in permissions.
Protected Routes
A protected route is a page that only some users may open. For example, /orders needs a logged-in user, and /admin needs an admin.
If the user may not open it, the app sends them somewhere else, usually to the login page. This is called a redirect.
A protected route: if the user is logged in, show the page. If not, redirect to the login page
/orders user asks for
logged in?
yes
Orders page shown
no
Redirect to /login
The guard sits in front of the page. Only logged-in users get through.
Route Guards Pattern in React
A route guard is a component that wraps a protected page. It checks the user first:
- If the check passes, it shows the page (
children). - If not, it redirects with React Router's
<Navigate>component.
function RequireAuth({ children }: { children: ReactNode }) {
const { user } = useAuth();
if (!user) {
return <Navigate to="/login" replace />;
}
return children;
}
// Use it around the page:
<Route path="/orders" element={<RequireAuth><OrdersPage /></RequireAuth>} />
<Navigate to="/login" replace /> changes the page as soon as it is shown. replace means the blocked page is not saved in the history, so the Back button does not lead back to it.
Click My orders while logged out. The address changes to /login. Now click Log in, then My orders again. This time the guard lets you through.
Authentication Context Pattern
Many components need to know who is logged in: the header, the guards, the account page. So the login state belongs in context, which you learned in the Context API lesson.
The pattern has three parts:
- an AuthContext that holds the user, plus
loginandlogoutfunctions; - an AuthProvider component near the top of the app;
- a useAuth hook, so any component can read it.
Auth Service Pattern
An auth service is a separate file that does the talking to the server: log in, read the token, check if it has expired. Components and context call the service. They do not contain server code themselves.
- Components show the screen.
- AuthContext remembers who is logged in.
- authService talks to the server and handles tokens.
This keeps each file small, and makes it easy to change the server code later without touching the screens.
Login State Management
The login state is the data that says who is logged in. Keep it small:
user— the details from the token payload, ornullwhen nobody is logged in.token— the JWT, needed to call the server.
From user, you can work out everything else: isLoggedIn is just user !== null. Also check the token's exp time. An expired token means the user must log in again.
Click Log in and wait. The seconds count down. At 0, the app treats the user as logged out, even though nobody pressed Log out. Real tokens usually last minutes or hours, not seconds.
Here is the full pattern together: an auth service, an auth context, a login page, route guards, and a role check.
Try these steps, and watch the address bar:
- Click My orders. You are not logged in, so
RequireAuthsends you to/login. - Press Log in (Asha's details are filled in). You go back to
/orders, the page you wanted. - Asha is a customer, so there is no Admin link. Log out, and log in as Ravi. Now the Admin link appears.
Click through the file tabs. Each file has one job: authService talks to the "server", AuthContext remembers the user, Guards protect the routes, and LoginPage shows the form.
Conditional Rendering Based on Authentication
Once you have user from context, showing different things is just conditional rendering, which you learned earlier.
const { user } = useAuth();
{user ? <p>Hi, {user.name}</p> : <Link to="/login">Log in</Link>}
{user && can(user.role, "edit-books") && <Link to="/admin">Admin</Link>}
Hiding a link is only for a nicer screen. It does not protect anything. A user can still type /admin in the address bar, so the route must also be guarded. And the server must check the token on every request.
Logout
Logout means forgetting the user. In a JWT app, that means:
- Remove the token from the app (set it to
null). - Clear any user data in state.
- Send the user to a public page, such as the home page.
function handleLogout() {
logout(); // token = null, so user = null
navigate("/"); // leave the protected page
}
A JWT stays valid until its exp time, even after logout. So real apps keep tokens short-lived, and the server can also keep a list of tokens that were logged out.
Authentication and Authorization Best Practices
- Always check on the server. Hiding links and guarding routes in React only helps the user. The server must check the token and the role on every request, because browser code can be changed.
- Never trust the token in the browser. The app reads the payload to show the user's name, but only the server can verify the signature.
- Never put secrets in a JWT. The payload can be read by anyone. No passwords, no card numbers.
- Use short expiry times. Check
exp, and log the user out when the token has expired. - Store tokens carefully. Many apps ask the server to keep the token in an httpOnly cookie, which JavaScript cannot read. A token in
localStoragecan be stolen if the site has a security hole that lets someone run their own script. - Always use HTTPS, so the password and token cannot be read on the way.
- Show general error messages, like "Wrong email or password". Do not say which one was wrong.
React guards are for a good user experience. Real security happens on the server.
Capstone Project: E-Commerce Access Management System
You will build a small shop where access depends on who you are. Guests can browse. Customers can register, log in and use a cart. Admins can manage product prices. It uses everything from this lesson: JWT, an auth context, route guards and roles.
E-commerce access flow: register or log in, get a JWT, keep it in the auth context, and let guards and roles decide what to show
Register or Log in
Server checks
JWT token
Auth context
Guards + roles
every page and link reads the user from the auth context
The token says who you are. The role says what you may do.
User Registration
register(name, email, password) in authService.ts checks the input (a name, a valid email, a password of 6+ characters, an email not used before), saves a new account with the role customer, and returns a token. New users never choose their own role: only the server gives out the admin role.
User Login
login(email, password) finds the account and returns a token, or throws "Wrong email or password." The login form shows that message and stays on the page.
JWT Authentication
Both functions return a JWT. Its payload holds the user's id, name, email, role and an expiry time (exp). The app reads the user from the token with readUser(token). In this playground the signature is fake; a real server signs the token with a secret key and checks it on every request.
Try both in the lab. Register a new account, then look at the token and the user read from it.
Role-Based Access Control (RBAC)
permissions in authService.ts lists what each role may do: a customer may use-cart, an admin may manage-products. can(role, action) answers yes or no. Pages and guards ask can(...), so the rules live in one place.
Protected Routes
Two guards in Guards.tsx protect the routes:
RequireAuthsends guests to/login, and remembers the page they wanted, so they go back there after logging in.RequirePermission action="manage-products"sends users without that permission to/no-access.
So /cart is for customers only, and /admin is for admins only, even if someone types the URL by hand.
Authentication State Management
AuthContext.tsx keeps the token in state and works out the user from it. It shares user, login, register and logout with every component. An expired token counts as logged out.
Each role sees different navigation links and pages
Guest
Customer
Admin
Shop · Log in · Register
Shop · 🛒 Cart Add to cart
Shop · ⚙️ Admin Edit prices
Same app, three views. Guards also block the URLs, not only the links.
User Navigation
NavBar shows links for the current user. Guests see Log in and Register. Customers see Cart with the item count. Everyone logged in sees their name, role and a Log out button.
Admin Navigation
Admins see an Admin link instead of the cart. The admin page lists products with a price box. Changing a price updates the shop at once.
Cart Management
StoreContext.tsx holds the products and the cart (product id → quantity). It gives addToCart, removeFromCart, cartCount and cartTotal. The cart is emptied when the user logs out or changes, so one person's cart never shows for another.
Conditional Rendering Based on Roles
The shop page shows a different action on each product card:
- Guest: Log in to buy
- Customer: Add to cart
- Admin: Edit price (a link to the admin page)
Here is the complete system. Try it as a guest, then log in as the customer (asha@shop.com / asha123), add items and open the cart. Log out and log in as the admin (admin@shop.com / admin123), change a price, then try to open the cart.
Hiding a link is not security. The real protection is on the server: it must check the JWT and the role on every API request, for example "only admins may change prices". The downloadable project below does exactly that.
Download Project Files
JWT Authentication Project Source Code
The complete shop is ready as a real project: a Vite + React + TypeScript front end and a small Express API that signs real JWTs with jsonwebtoken, stores hashed passwords with bcryptjs, and checks the token and role on every protected request.
Download JWT Authentication Project: Download E-Commerce Access Management Project
ecommerce-access/
package.json scripts for the web app and the API
.env.example JWT_SECRET and token lifetime
server/
index.js Express API: register, login, products, cart, admin
auth.js sign and verify JWTs, requireAuth and requireRole middleware
db.js JSON file storage (users, products, carts)
data/ users.json, products.json, carts.json
src/
main.tsx, App.tsx providers and routes
api/client.ts fetch helper that adds "Authorization: Bearer <token>"
context/ AuthContext (token + user), CartContext
auth/ permissions (RBAC), ProtectedRoute, RoleRoute
components/ NavBar, ProductCard
pages/ Shop, Login, Register, Cart, Admin, NoAccess, NotFound
Project Setup Instructions
- Install Node.js 18 or newer from nodejs.org, if you do not have it.
- Download the ZIP with the link above, and unzip it. You get a folder called
ecommerce-access. - Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
- Install the packages and start both the API and the web app:
npm install
npm run dev
Open http://localhost:5173. Log in as asha@shop.com / asha123 (customer) or admin@shop.com / admin123 (admin), or register a new customer. The API runs at http://localhost:3001. Press Ctrl + C to stop both.
Common mistakes
| Mistake | What you see | Fix |
|---|---|---|
Hiding the Admin link but not guarding /admin | Anyone who types /admin sees the page | Wrap the route in a guard, and check on the server |
| Putting a password in the JWT payload | Anyone with the token can read the password | Only put non-secret details like id, name and role |
Not checking exp | Users stay "logged in" with an expired token | Treat an expired token as logged out |
Forgetting replace on the login redirect | Back button returns to the blocked page, and redirects again | Use <Navigate to="/login" replace /> |
| Checking user names instead of roles | Code breaks when users change | Check roles or permissions with a can() function |
Using useAuth outside AuthProvider | Error: useAuth must be used inside AuthProvider | Wrap the app in <AuthProvider> |
| Not sending the user back after login | They land on the home page and must find their page again | Save from in the redirect, and navigate there after login |
Check Yourself
Exercises
What to Remember
- Authentication checks who the user is. Authorization checks what they may do. Authentication comes first.
- After login, the server gives a JWT. The app keeps it, reads the user from its payload, and sends it with each request.
- Keep the user in an auth context, protect routes with guards, check roles with a
can()function, and always check again on the server.
Next: Client-Side Rendering