Learn ReactJS - Secure Frontend & Auth Patterns
Episode 12 of 24

Learn ReactJS - Secure Frontend & Auth Patterns

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.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

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.

Authentication Patterns for SPAs

The Login Flow in a Single Page 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:

SPA authentication flow
login -> server verifikasi -> token dikirim -> token disimpan -> token dipakai di header Authorization

The 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.

JWT, Refresh Tokens, and Secure Storage

Access Token and Refresh Token

A single long-lived token is a huge risk if it leaks. The safer pattern uses two tokens:

  • Access token: short-lived, for example 15 minutes, sent on every request.
  • Refresh token: long-lived, only used to obtain a new access token.
JSUsing an access token with axios
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:

  • localStorage: easily readable by JavaScript, so it's vulnerable to XSS. Only suitable for non-sensitive tokens.
  • HttpOnly cookie: inaccessible to JavaScript, so it's safe from XSS. Must be paired with 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.

Protecting Routes and Role-Based Access Control

Protecting Routes

Combine the route guard pattern from episode 9 with an authentication context:

JSProtected route with auth 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>.

Role-Based Access Control

Not all users have the same rights. RBAC checks the user's role before allowing an action or page:

JSRole check
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.

CSRF, XSS, and Secure Handling of User Input

XSS and React

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:

JSUsing dangerouslySetInnerHTML safely
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 and HttpOnly Cookies

CSRF happens when the browser unknowingly sends requests to your site carrying session cookies. Main mitigations:

  • Set cookies with SameSite=Lax or Strict.
  • Use a CSRF token in the header for state-changing requests.
  • Never rely on cookies as the only authentication layer.
Install HTML sanitizer
npm install dompurify

The 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.

Conclusion

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:

  • SPA authentication: log in, receive a token, send it in the Authorization header.
  • JWT is used as the access token; the refresh token renews it without re-login.
  • An HttpOnly cookie is safer against XSS than localStorage.
  • Frontend protection is only for UX; true authorization lives on the server.
  • React escapes text by default; sanitize content before using dangerouslySetInnerHTML.
  • Cookies need 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.

Learn ReactJS - Secure Frontend & Auth Patterns | Learn ReactJS