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.

AuthenticationAuthorization
QuestionWho are you?What can you do?
WhenFirst, at loginAfter login, on every protected action
ChecksEmail 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"
ExampleAsha logs inAsha 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.

  1. The user types an email and password in the login form.
  2. The app sends them to the server's login API.
  3. The server checks them. If they are correct, it creates a JWT and sends it back.
  4. The app keeps the token, and stores the user's details in state.
  5. 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, and exp (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 login and logout functions;
  • 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, or null when 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:

  1. Click My orders. You are not logged in, so RequireAuth sends you to /login.
  2. Press Log in (Asha's details are filled in). You go back to /orders, the page you wanted.
  3. 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:

  1. Remove the token from the app (set it to null).
  2. Clear any user data in state.
  3. 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 localStorage can 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:

  • RequireAuth sends 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

  1. Install Node.js 18 or newer from nodejs.org, if you do not have it.
  2. Download the ZIP with the link above, and unzip it. You get a folder called ecommerce-access.
  3. Open a terminal in that folder (in VS Code: File, Open Folder, then Terminal, New Terminal).
  4. 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

MistakeWhat you seeFix
Hiding the Admin link but not guarding /adminAnyone who types /admin sees the pageWrap the route in a guard, and check on the server
Putting a password in the JWT payloadAnyone with the token can read the passwordOnly put non-secret details like id, name and role
Not checking expUsers stay "logged in" with an expired tokenTreat an expired token as logged out
Forgetting replace on the login redirectBack button returns to the blocked page, and redirects againUse <Navigate to="/login" replace />
Checking user names instead of rolesCode breaks when users changeCheck roles or permissions with a can() function
Using useAuth outside AuthProviderError: useAuth must be used inside AuthProviderWrap the app in <AuthProvider>
Not sending the user back after loginThey land on the home page and must find their page againSave 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