Medicare Frontend Authentication with JWT & Protected Routes

Business Scenario

In this lab, we will integrate the Medicare React frontend with the existing Spring Boot authentication system by implementing user registration, login, and JWT-based authentication.

Pre-Lab Preparation

Module:

1) React Basics & Backend Integration (Spring Boot)​

Register → Name + Email + Phone + Password
Role → PATIENT automatically
Login → Email + Password
        ↓
JWT
  • Now we connect React to our real Spring Boot authentication system.

Phase 12B — Authentication, JWT & Protected Routes

  • This directly implements the original project requirement:
        ↓
Patient / Doctor Dashboard
  • Our backend already has the required endpoints:
POST /api/auth/register
POST /api/auth/login

Configure CORS in Spring Boot

1

C:\MEDICAREPROJECT\medicare-backend
  • Our React application runs on:
  • Backend runs on:
http://localhost:8080
  • Different ports mean different origins.
  • So the browser needs permission from Spring Boot.
  • Spring Security recommends configuring CORS so that preflight requests are handled correctly before security rejects them.
  • Open: SecurityConfig.java Add these imports:
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
  • Then add :
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {

    CorsConfiguration configuration = new CorsConfiguration();

    configuration.setAllowedOrigins(
            List.of("http://localhost:5173")
    );

    configuration.setAllowedMethods(
            List.of("GET","POST","PUT","PATCH","DELETE","OPTIONS")
    );

    configuration.setAllowedHeaders(
            List.of("Authorization","Content-Type")
    );

    configuration.setAllowCredentials(false);

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    source.registerCorsConfiguration("/**",configuration);

    return source;
}
.cors(cors -> cors.configurationSource(
                corsConfigurationSource()
        )
)
  • Then inside your existing:
SecurityFilterChain securityFilterChain(...)
  • Important :Don't replace your existing:
JWT filter
Authorization rules
SessionCreationPolicy.STATELESS
CustomAuthenticationEntryPoint
CustomAccessDeniedHandler

Check .env

2

  • Your React frontend should already contain:
medicare-frontend/.env
  • with:
VITE_API_BASE_URL=http://localhost:8080/api
  • Vite exposes client environment variables prefixed with VITE_ through import.meta.env. ​

Create authService.js

3

src/services/authService.js
import api from "../api/api";

const register = async (registerData) => {
    const response = await api.post("/auth/register",registerData);
    return response.data;
};

const login = async (loginData) => {
    const response = await api.post("/auth/login",loginData);
    return response.data;
};

const authService = {register,login};

export default authService;

Understand Our Login Response

4

  • Our backend returns something conceptually like:
{
    "token": "eyJhbGciOiJIUzI1NiJ9...",
    "tokenType": "Bearer",
    "userId": 1,
    "name": "Rahul Shah",
    "email": "rahul@gmail.com",
    "role": "PATIENT"
}
{
    "token": "eyJhbGciOiJIUzI1NiJ9...",
    "tokenType": "Bearer",
    "userId": 2,
    "name": "Dr. Ananya",
    "email": "ananya@gmail.com",
    "role": "DOCTOR"
}
  • We're going to store: token, user separately.

Create AuthContext

5

  • Create:
src/context/AuthContext.jsx
import {createContext,useContext,useState} from "react";

const AuthContext = createContext(null);

function getStoredUser() {
    const storedUser = localStorage.getItem("user");

    if (!storedUser) {
        return null;
    }

    try {
        return JSON.parse(storedUser);
    } catch {
        localStorage.removeItem("user");
        return null;
    }
}


export function AuthProvider({ children }) {

    const [token, setToken] = useState(() =>
            localStorage.getItem("token")
        );

    const [user, setUser] = useState(getStoredUser);


    const login = (authResponse) => {
        const loggedInUser = {
            userId: authResponse.userId,
            name: authResponse.name,
            email: authResponse.email,
            role: authResponse.role
        };

        localStorage.setItem("token",authResponse.token);
        localStorage.setItem("user",JSON.stringify(loggedInUser));
        setToken(authResponse.token);
        setUser(loggedInUser);
    };
    const logout = () => {
        localStorage.removeItem("token");
        localStorage.removeItem("user"
        );
        setToken(null);
        setUser(null);
    };


    const isAuthenticated = Boolean(token && user);

    return (
        <AuthContext.Provider
            value={{user,token,isAuthenticated,login,logout
            }}>
            {children}
        </AuthContext.Provider>
    );
}

export function useAuth() {
    return useContext( AuthContext);
}

Why AuthContext?

  • Without Context:
Login
 ↓Patient Dashboard
 ↓Doctor Search
 ↓Appointments
 ↓Profile
  • Every page would need its own authentication state.
             AuthContext
                 │
      ┌──────────┼──────────┐
      ↓          ↓          ↓
   Navbar    Dashboard    Routes
  • Everyone can access:
const {
    user,
    token,
    login,
    logout,
    isAuthenticated
} = useAuth();

Why localStorage AND Context?

  • Context gives us: React application state
  • But refreshing the browser destroys normal React state.
  • Therefore:
localStorage
      ↓
persists JWT + user

AuthContext
      ↓
provides that information through React

Wrap Application with AuthProvider

6

  • Open : src/main.jsx
  • Replace it with:
import React from "react";
import ReactDOM from "react-dom/client";
import {RouterProvider} from "react-router/dom";
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap/dist/js/bootstrap.bundle.min.js";
import "./index.css";
import router from "./routes/router";
import {AuthProvider} from "./context/AuthContext";


ReactDOM.createRoot(document.getElementById("root")).render(
    <React.StrictMode>
        <AuthProvider>
            <RouterProvider
                router={router}/>
        </AuthProvider>
    </React.StrictMode>
);

Build Real Registration Page

7

  • Open: src/pages/auth/Register.jsx
  • Replace it with:
import { useState } from "react";
import { Link, useNavigate } from "react-router";
import authService from "../../services/authService";

function Register() {
    const navigate = useNavigate();

    const [formData, setFormData] = useState({
        name: "",
        email: "",
        phone: "",
        password: ""
    });

    const [error, setError] = useState("");
    const [loading, setLoading] = useState(false);

    const handleChange = (event) => {
        const { name, value } = event.target;
        setFormData((previous) => ({
            ...previous,
            [name]: value
        }));
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError("");
        setLoading(true);

        try {
            await authService.register(formData);
            navigate("/login", {
                state: {
                    message: "Registration successful. Please login."
                }
            });
        } catch (error) {
            setError(
                error.response?.data?.message || "Registration failed"
            );
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-md-7 col-lg-6">
                    <div className="card shadow-sm">
                        <div className="card-body p-4">
                            <h2 className="text-center mb-4">Create Account</h2>

                            {error && (
                                <div className="alert alert-danger">
                                    {error}
                                </div>
                            )}

                            <form onSubmit={handleSubmit}>
                                <div className="mb-3">
                                    <label className="form-label">Name</label>
                                    <input
                                        type="text"
                                        name="name"
                                        className="form-control"
                                        value={formData.name}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <div className="mb-3">
                                    <label className="form-label">Email</label>
                                    <input
                                        type="email"
                                        name="email"
                                        className="form-control"
                                        value={formData.email}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <div className="mb-3">
                                    <label className="form-label">Phone Number</label>
                                    <input
                                        type="tel"
                                        name="phone"
                                        className="form-control"
                                        value={formData.phone}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <div className="mb-3">
                                    <label className="form-label">Password</label>
                                    <input
                                        type="password"
                                        name="password"
                                        className="form-control"
                                        value={formData.password}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <button
                                    type="submit"
                                    className="btn btn-primary w-100"
                                    disabled={loading}
                                >
                                    {loading ? "Creating Account..." : "Register"}
                                </button>
                            </form>

                            <p className="text-center mt-4 mb-0">
                                Already have an account?{" "}
                                <Link to="/login">Login</Link>
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}
export default Register;

Very Important — No Role Field

  • We deliberately did NOT create:
Role:
[ PATIENT ]
[ DOCTOR ]
  • Because public users should never be able to send:
{ "role": "DOCTOR" }
  • Registration sends only:
{
    "name": "Rahul Shah",
    "email": "rahul@gmail.com",
    "phone": "9876543210",
    "password": "password"
}
  • Backend automatically sets: PATIENT

Build Real Login Page

8

  • Open: src/pages/auth/Login.jsx
  • Replace it:


import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router";
import authService from "../../services/authService";
import { useAuth } from "../../context/AuthContext";

function Login() {
    const navigate = useNavigate();
    const location = useLocation();
    const { login } = useAuth();

    const [formData, setFormData] = useState({
        email: "",
        password: ""
    });

    const [error, setError] = useState("");
    const [loading, setLoading] = useState(false);

    const handleChange = (event) => {
        const { name, value } = event.target;
        setFormData((previous) => ({
            ...previous,
            [name]: value
        }));
    };

    const handleSubmit = async (event) => {
        event.preventDefault();
        setError("");
        setLoading(true);
        try {
            const response = await authService.login(formData);
            login(response);

            if (response.role === "PATIENT") {
                navigate("/patient/dashboard");
            } else if (response.role === "DOCTOR") {
                navigate("/doctor/dashboard");
            } else {
                navigate("/unauthorized");
            }
        } catch (error) {
            setError(
                error.response?.data?.message || "Invalid email or password"
            );
        } finally {
            setLoading(false);
        }
    };

    return (
        <div className="container py-5">
            <div className="row justify-content-center">
                <div className="col-md-6 col-lg-5">
                    <div className="card shadow-sm">
                        <div className="card-body p-4">
                            <h2 className="text-center mb-4">Login</h2>

                            {location.state?.message && (
                                <div className="alert alert-success">
                                    {location.state.message}
                                </div>
                            )}

                            {error && (
                                <div className="alert alert-danger">
                                    {error}
                                </div>
                            )}

                            <form onSubmit={handleSubmit}>
                                <div className="mb-3">
                                    <label className="form-label">Email</label>
                                    <input
                                        type="email"
                                        name="email"
                                        className="form-control"
                                        value={formData.email}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <div className="mb-3">
                                    <label className="form-label">Password</label>
                                    <input
                                        type="password"
                                        name="password"
                                        className="form-control"
                                        value={formData.password}
                                        onChange={handleChange}
                                        required
                                    />
                                </div>

                                <button
                                    type="submit"
                                    className="btn btn-primary w-100"
                                    disabled={loading}
                                >
                                    {loading ? "Logging in..." : "Login"}
                                </button>
                            </form>

                            <p className="text-center mt-4 mb-0">
                                Don't have an account?{" "}
                                <Link to="/register">Register</Link>
                            </p>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    );
}

export default Login;

Create ProtectedRoute

9

  • Create : src/routes/ProtectedRoute.jsx
import { Navigate } from "react-router";
import { useAuth } from "../context/AuthContext";

function ProtectedRoute({ children, allowedRoles = [] }) {
    const { user, isAuthenticated } = useAuth();

    if (!isAuthenticated) {
        return <Navigate to="/login" replace />;
    }

    if (allowedRoles.length > 0 && !allowedRoles.includes(user?.role)) {
        return <Navigate to="/unauthorized" replace />;
    }

    return children;
}

export default ProtectedRoute;

Protect Routes

10

  • Open:src/routes/router.jsx
import { createBrowserRouter } from "react-router";
import Home from "../pages/common/Home";
import Login from "../pages/auth/Login";
import Register from "../pages/auth/Register";
import PatientDashboard from "../pages/patient/PatientDashboard";
import DoctorDashboard from "../pages/doctor/DoctorDashboard";
import Unauthorized from "../pages/common/Unauthorized";
import NotFound from "../pages/common/NotFound";
import ProtectedRoute from "./ProtectedRoute";

const router = createBrowserRouter([
    {
        path: "/",
        element: <Home />
    },
  • Replace it with: 
  • Open:src/routes/router.jsx
   {
        path: "/login",
        element: <Login />
    },
    {
        path: "/register",
        element: <Register />
    },
    {
        path: "/patient/dashboard",
        element: (
            <ProtectedRoute allowedRoles={["PATIENT"]}>
                <PatientDashboard />
            </ProtectedRoute>
        )
    },
    {
        path: "/doctor/dashboard",
        element: (
            <ProtectedRoute allowedRoles={["DOCTOR"]}>
                <DoctorDashboard />
            </ProtectedRoute>
        )
    },
    {
        path: "/unauthorized",
        element: <Unauthorized />
    },
    {
        path: "*",
        element: <NotFound />
    }
]);

export default router;

What ProtectedRoute Is Doing

  • Suppose
Not logged in
  • And user manually types:
/patient/dashboard
  • Result: /login
  • Suppose logged-in patient enters:
/doctor/dashboard
  • Result: /unauthorized

Add Logout to Patient Dashboard

11

  • For now we'll add a basic logout button so authentication can be properly tested.
  • Open: PatientDashboard.jsx
  • Replace with:
import { useNavigate } from "react-router";
import { useAuth } from "../../context/AuthContext";

function PatientDashboard() {
    const navigate = useNavigate();
    const { user, logout } = useAuth();

    const handleLogout = () => {
        logout();
        navigate("/login");
    };

    return (
        <div className="container py-5">
            <div className="d-flex justify-content-between align-items-center mb-4">
                <div>
                    <h2>Patient Dashboard</h2>
                    <p className="text-secondary mb-0">
                        Welcome, {user?.name}
                    </p>
                </div>

                <button
                    className="btn btn-outline-danger"
                    onClick={handleLogout}
                >
                    Logout
                </button>
            </div>

            <div className="alert alert-info">
                Patient healthcare modules will be added next.
            </div>
        </div>
    );
}

export default PatientDashboard;

Add Logout to Doctor Dashboard

12

  • Open: DoctorDashboard.jsx
  • Replace it with:
import { useNavigate } from "react-router";
import { useAuth } from "../../context/AuthContext";

function DoctorDashboard() {
    const navigate = useNavigate();
    const { user, logout } = useAuth();

    const handleLogout = () => {
        logout();
        navigate("/login");
    };

    return (
        <div className="container py-5">
            <div className="d-flex justify-content-between align-items-center mb-4">
                <div>
                    <h2>Doctor Dashboard</h2>
                    <p className="text-secondary mb-0">
                        Welcome, {user?.name}
                    </p>
                </div>

                <button className="btn btn-outline-danger"
                    onClick={handleLogout}>
                    Logout
                </button>
            </div>

            <div className="alert alert-info">
                Doctor appointment modules will be added next.
            </div>
        </div>
    );
}

export default DoctorDashboard;
  • Later the logout button moves into our global Navbar.
  • For now this lets us verify authentication.

Restart Both Applications

13

Test Registration

14

  • Go to: http://localhost:5173/register
  • Enter:
Name: Rahul Shah
Email: rahul@gmail.com
Phone: 9876543210
Password: your password
  • Click: Register
  • Frontend sends: POST /api/auth/register
  • Body:
{
    "name": "Rahul Shah",
    "email": "rahul@gmail.com",
    "phone": "9876543210",
    "password": "your password"
}
  • Backend should create: User role = PATIENT
  • Then React redirects: /login with:
    ​Registration successful. Please login. 

Test Patient Login

15

  • Login using the patient account.
  • Frontend sends:
{
    "email": "rahul@gmail.com",
    "password": "your password"
}
  • Backend returns JWT.
  • Then :
  • React stores:
localStorage
token
user
PATIENT --> /patient/dashboard

Inspect Browser Storage

16

  • Open browser Developer Tools:
Application
   ↓
Local Storage
   ↓
http://localhost:5173
  • You should see approximately:
token : eyJhbGciOiJIUzI1NiJ9...

user : {"userId":1,"name":"Rahul Shah","email":"rahul@gmail.com","role":"PATIENT"}

Test Browser Refresh

17

  • While on: /patient/dashboard   press: F5
  • The user should remain logged in.
localStorage
      ↓
AuthContext initialization
      ↓
token restored
user restored
  • Why?

Test Patient Security

18

403
Access Denied
  • While logged in as PATIENT manually type: http://localhost:5173/doctor/dashboard
  • Expected:

Test Doctor Login

19

  • Because public registration only creates patients, use the doctor account you already created for backend testing.
Doctor Email
Doctor Password
  • Expected:
{
    "role": "DOCTOR"
}
  • Backend returns:
  • React redirects: /doctor/dashboard

Test Doctor Security

20

  • Doctor manually enters: /patient/dashboard
  • Expected /unauthorized

Test Logout

21

  • Click: Logout
  • React performs:
localStorage.removeItem("token");
localStorage.removeItem("user");
  • Context becomes:
user = null
token = null
  • Then: /login
  • Try manually entering: /patient/dashboard
  • Expected: /login
                 REGISTER
                    │
                    ▼
             Register.jsx
                    │
                    ▼
            authService.js
                    │
                    ▼
       POST /api/auth/register
                    │
                    ▼
               Spring Boot
                    │
             role = PATIENT

Complete Authentication Architecture

                  LOGIN
                    │
                    ▼
               Login.jsx
                    │
                    ▼
             authService.js
                    │
                    ▼
         POST /api/auth/login
                    │
                    ▼
             Spring Security
                    │
                    ▼
                   JWT
                    │
                    ▼
               AuthContext
                    │
          ┌─────────┴─────────┐
          ▼                   ▼
       Context           localStorage
          │
          ▼
    ProtectedRoute
          │
     ┌────┴─────┐
     ▼          ▼
  PATIENT     DOCTOR
     │          │
     ▼          ▼
 Patient      Doctor
Dashboard    Dashboard

We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below: 

 

Great job!

You have successfully implemented the Medicare frontend authentication workflow using React, JWT, and protected routes.

Checkpoint

   Git Push

git push origin branchName

Next-Lab Preparation

Module:

1) React Basics & Backend Integration (Spring Boot)​

SpringBoot Lab - 14

By Content ITV

SpringBoot Lab - 14

  • 102