Skip to content

Frontend Authentication Flow

intermediate2 min read
  • React
  • Authentication
  • JWT

The client side of authentication: logging in, storing the resulting credential safely, attaching it to requests, gating routes, and handling logout.

Prerequisites

  • Authentication and JWT basics from the backend modules (see Related Topics)

What you'll understand

  • What the frontend is responsible for in authentication
  • The tradeoffs of where a credential is stored
  • How the client gates routes while the server stays the real authority

Explanation

Authentication is proven on the server (see Related Topics), but the frontend runs the flow the user experiences. It has four jobs: send the login credentials, hold on to whatever credential the server issues, attach that credential to future requests, and reflect the signed-in or signed-out state in the UI, including logging out. The server verifies; the client orchestrates.

Where to keep the issued credential is the central decision, and it is a security tradeoff. A token in JavaScript-accessible storage such as localStorage is easy to use but readable by any script, so it is exposed to cross-site scripting (XSS). A token in an httpOnly cookie cannot be read by JavaScript, which defends against XSS, but requires guarding against cross-site request forgery (CSRF). There is no free option; httpOnly cookies are generally the safer default, and secrets must never be logged or embedded in the app.

Once the client holds a credential, it attaches it to each request (an Authorization header or an automatically sent cookie) and uses the known auth state to gate the UI, redirecting anonymous users away from protected routes. But this gating is only user experience, not security: a determined user can bypass any client check, so the server must still authorize every request itself (see Related Topics). The client also handles expiry, refreshing or forcing a fresh login when the credential is no longer valid.

Examples

After a successful login the client stores the returned state and attaches the credential to later requests:

async function login(email: string, password: string) {
  const res = await fetch('/api/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error('Login failed');
  // Prefer the server to set an httpOnly cookie; otherwise store the returned token.
}

A protected route checks the known auth state and redirects when the user is not signed in (a UX guard, not the real gate):

function Protected({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  if (!user) return <Navigate to="/login" />;
  return <>{children}</>;
}

Common mistakes

  • Treating client-side route guards as real security instead of a UX convenience.
  • Storing tokens in localStorage without weighing the XSS exposure.
  • Logging or embedding tokens and secrets in the frontend.
  • Ignoring credential expiry, leaving the user in a broken half-signed-in state.

Best practices

  • Let the server remain the authority; never rely on the client to enforce access (see Related Topics).
  • Prefer httpOnly cookies for the credential and understand the CSRF tradeoff.
  • Attach the credential consistently to every authenticated request.
  • Handle expiry and logout explicitly, clearing client auth state.

Further reading

Related topics