This episode covers authentication patterns for SPAs, the JWT flow with refresh tokens and secure storage, protecting routes and role-based access control, and mitigating CSRF and XSS when handling user input on the frontend.

Frontend security is often underestimated, even though this is where user data is first touched. Episode 12 covers the correct patterns for authentication and authorization in React apps: the login flow in SPAs, secure token storage, protecting routes, and role-based access control.
In the second half we discuss the real threats that haunt every frontend — CSRF and XSS — and the practices for handling user input safely. Security isn't an add-on feature; it's a basic requirement of a serious app.
In an SPA, authentication generally works like this: the user sends credentials, the server returns a token, the app stores it and uses it on every subsequent request:
login -> server verifikasi -> token dikirim -> token disimpan -> token dipakai di header AuthorizationThe most common token is the JWT (JSON Web Token): a string containing a header, payload, and signature that the server can verify without storing a session. The app puts it in the Authorization: Bearer <token> header on each request.
A single long-lived token is a huge risk if it leaks. The safer pattern uses two tokens:
import axios from "axios"
const api = axios.create({
baseURL: "https://api.example.com",
})
api.interceptors.request.use((config) => {
const token = localStorage.getItem("access_token")
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})The interceptor api.interceptors.request.use((config) => ...) adds the Authorization header automatically to every request. When the access token expires, a response interceptor can call a refresh endpoint to renew it without forcing the user to log in again.
This is an important security decision:
Secure and SameSite to defend against CSRF.For production apps, the recommended pattern: access token in memory (React state) and refresh token in an HttpOnly cookie. Never put sensitive tokens in localStorage without weighing the XSS risk.
Combine the route guard pattern from episode 9 with an authentication context:
import { useContext } from "react"
import { Navigate } from "react-router-dom"
import { AuthContext } from "./AuthContext.jsx"
function ProtectedRoute({ anak }) {
const { user } = useContext(AuthContext)
if (!user) return <Navigate to="/login" replace />
return anak
}const { user } = useContext(AuthContext) reads the user session. If there's no user, <Navigate to="/login" replace /> redirects to login. Wrap sensitive routes with <ProtectedRoute>...</ProtectedRoute>.
Not all users have the same rights. RBAC checks the user's role before allowing an action or page:
function TombolHapus({ user, onClick }) {
if (user.role !== "admin") return null
return <button onClick={onClick}>Hapus</button>
}user.role !== "admin" hides the button from non-admin users. Remember the golden rule of security: frontend protection is only for UX — real authorization must always run on the server.
XSS happens when untrusted content is rendered as HTML. React escapes all text by default — writing {userInput} is safe. Danger appears when you use dangerouslySetInnerHTML or surface text in attributes the wrong way:
import { DOMPurify } from "dompurify"
function Konten({ html }) {
const bersih = DOMPurify.sanitize(html)
return <div dangerouslySetInnerHTML={{ __html: bersih }} />
}DOMPurify.sanitize(html) cleans dangerous tags before rendering. Use dangerouslySetInnerHTML only for content that has been sanitized, and avoid it for direct user input.
CSRF happens when the browser unknowingly sends requests to your site carrying session cookies. Main mitigations:
SameSite=Lax or Strict.npm install dompurifyThe npm install dompurify command installs the HTML sanitization library. Always sanitize content coming from outside before rendering it — this is one of the most impactful security habits.
Episode 12 equipped you with frontend security: the SPA authentication flow with JWT, the access and refresh token pattern, secure token storage, protecting routes with RBAC, and mitigating CSRF and XSS.
Key takeaways:
Authorization header.dangerouslySetInnerHTML.SameSite and Secure to resist CSRF.In the next episode, episode 13, we'll cover API integration & error handling — consuming REST and GraphQL APIs, fetch policies with retries and retry delays, centralized error handling with fallback UI, plus preloading data and optimistic updates. Robust integration is what sets professional apps apart.