Scientyfic World

Login Authentication Without a Backend: Why It’s Unsafe and What to Use Instead

You can’t implement secure login authentication without a backend, because anything that runs in the browser, including passwords, secret keys and the “is logged in” check, can be read and...

Share:

Get an AI summary of this article

Feature image for blog on Login Authentication containing a lady working on a desktop

You can’t implement secure login authentication without a backend, because anything that runs in the browser, including passwords, secret keys and the “is logged in” check, can be read and changed by the user. What you can do is skip building your own backend: managed authentication services such as Firebase Authentication, Supabase Auth, Auth0 or Clerk run the server side for you, so your front end only calls their SDK.

I wrote the original version of this post for individuals and small projects that don’t have the resources to build a full backend login system. That goal still stands, but the approach it showed (hashing passwords and signing JWTs in the browser) is not safe, so this updated version explains why, demonstrates the problem, and shows the approach I’d recommend instead, tested end to end.

Why Client-Side-Only Login Is Not Secure

To check that I wasn’t overstating the risk, I built a small React app the way many “login without backend” tutorials do, with the user list, password and JWT secret in the front-end code:

// DON'T DO THIS: "authentication" entirely in the browser
const SECRET_KEY = 'my-super-secret-jwt-key';
const USERS = { '[email protected]': 'hunter2' };

export function login(email, password) {
  if (USERS[email] === password) {
    localStorage.setItem('token', btoa(JSON.stringify({ email, sig: SECRET_KEY })));
    return true;
  }
  return false;
}

export const isLoggedIn = () => Boolean(localStorage.getItem('token'));

export default function App() {
  return isLoggedIn() ? <h1>Admin dashboard</h1> : <button onClick={() => login('[email protected]', 'hunter2')}>Log in</button>;
}

Then I tested it the way an attacker would:

  1. Reading the bundle. After a normal production build (minified by Vite), searching the JavaScript file found my-super-secret-jwt-key, [email protected] and hunter2 in plain text. Anyone can open DevTools, view the source and copy them. Minification is not encryption.
  2. Skipping the login. In the browser console I ran localStorage.setItem('token', 'anything') and reloaded. The app showed “Admin dashboard” without any password.

The underlying rule: the browser is controlled by the user. Every check that runs there can be edited, and every value shipped to it can be read. A JWT is only meaningful if it is signed with a key the user never sees and verified by code the user can’t change, which means a server. Storing tokens in localStorage also exposes them to any cross-site scripting bug (see the OWASP HTML5 security cheat sheet).

Client-side “login” is fine only where nothing is really protected: a demo, a prototype, or hiding UI that holds no private data.

Your Options for Login Without Building a Backend

OptionWhat it gives youFree tierGood for
Firebase AuthenticationEmail/password, phone, Google, Apple, GitHub and more; SDKs for web and mobileGenerous for most small appsApps already using Firebase or Firestore
Supabase AuthEmail, magic links, OAuth; integrates with Postgres row-level securityYesApps that want a Postgres database too
Auth0Hosted login page, social and enterprise logins, MFAYes, with limitsTeams needing enterprise SSO or a hosted login UI
ClerkDrop-in React components for sign-in, sign-up and user profileYes, with limitsReact and Next.js apps that want ready-made UI

All of them hash passwords, issue signed tokens, handle password resets and email verification, and protect against brute-force attacks, which is the part that is hard to get right yourself. Pricing and limits change, so check each provider’s pricing page before you commit.

Example: Email and Password Login with Firebase Authentication

Here is the whole flow, client and server, which I ran against the Firebase Authentication emulator (firebase 13.1.0, firebase-admin 14.5.0, firebase-tools 15.34.1). The first half is exactly what your React app calls; the second half is what your API or a serverless function runs to trust a request:

// Client side: what your React app runs (firebase JS SDK)
import { initializeApp } from 'firebase/app';
import {
  getAuth, connectAuthEmulator, createUserWithEmailAndPassword,
  signInWithEmailAndPassword, signOut,
} from 'firebase/auth';

const app = initializeApp({ apiKey: 'demo-key', projectId: 'demo-login' });
const auth = getAuth(app);
connectAuthEmulator(auth, 'http://127.0.0.1:9099', { disableWarnings: true });

const { user } = await createUserWithEmailAndPassword(auth, '[email protected]', 'correct horse battery');
console.log('signed up:', user.email, '| uid:', user.uid.slice(0, 8) + '...');
await signOut(auth);

try {
  await signInWithEmailAndPassword(auth, '[email protected]', 'wrong password');
} catch (err) {
  console.log('wrong password ->', err.code);
}

const cred = await signInWithEmailAndPassword(auth, '[email protected]', 'correct horse battery');
const idToken = await cred.user.getIdToken();
console.log('signed in, ID token length:', idToken.length);

// Server side: what your API (or a serverless function) runs (firebase-admin)
import { initializeApp as initAdmin } from 'firebase-admin/app';
import { getAuth as getAdminAuth } from 'firebase-admin/auth';
const admin = initAdmin({ projectId: 'demo-login' });
const decoded = await getAdminAuth(admin).verifyIdToken(idToken);
console.log('server verified token for:', decoded.email);

try {
  await getAdminAuth(admin).verifyIdToken(idToken.slice(0, -5) + 'xxxxx');
} catch (err) {
  console.log('tampered token ->', err.code);
}

Output:

signed up: [email protected] | uid: K0zT0qbS...
wrong password -> auth/wrong-password
signed in, ID token length: 491
server verified token for: [email protected]
tampered token -> auth/argument-error

Notes from the test:

  • No secrets in the front end. The apiKey in a Firebase config is an identifier, not a secret; security comes from Firebase’s servers and your security rules.
  • Error codes differ in production. The emulator returned auth/wrong-password, but projects with email enumeration protection (the default for new projects) return auth/invalid-credential for both a wrong password and an unknown email, so attackers can’t discover which emails are registered. Show the user one generic “email or password is incorrect” message.
  • The server must verify the token. Send the ID token in an Authorization: Bearer header and verify it with the Admin SDK on every request. Emulator tokens are not signed, so test signature checks against a real project before you go live.
  • If you use Firestore or Storage directly from the client, security rules are your backend: lock data to request.auth.uid.

For a complete React integration with sign-up, login, logout and protected pages, follow my Firebase Authentication with React guide; for mobile, see Firebase Auth with Expo. If you prefer a hosted login page, see how to create an Auth0 protected route in React.

What If I Need a Little Server Logic?

You still don’t need to run a server. Serverless functions (Firebase Cloud Functions, Supabase Edge Functions, Vercel or Netlify Functions, Cloudflare Workers) let you write small pieces of trusted code, such as verifying a token before returning private data or calling a paid API with a secret key. They run on the provider’s infrastructure, so the secret never reaches the browser. I covered one such setup in how to optimize Supabase Edge Functions.

Security Checklist for Front-End Login

  • Never put passwords, API secrets or signing keys in front-end code or environment variables that are bundled (VITE_ and NEXT_PUBLIC_ variables are public).
  • Let the provider hash and store passwords; never store them yourself.
  • Verify tokens on the server, or with security rules, for every protected read and write.
  • Treat route guards in React as user experience only, not as security.
  • Prefer the provider’s session handling over copying tokens into localStorage yourself, and fix XSS bugs, since any script on your page can act as the user.
  • Turn on email verification, rate limiting and multi-factor authentication where your provider supports them.

Conclusion

Real authentication always needs a trusted server, because the browser can’t keep secrets or enforce rules. The good news is that you don’t have to build that server: Firebase Authentication, Supabase Auth, Auth0 and Clerk handle passwords, tokens and sessions for you, and your front end just calls their SDK. Use one of them, verify tokens wherever data is protected, and keep secrets out of your JavaScript bundle.

FAQs

Can I implement login authentication without a backend?

Not securely on your own. Anything in the browser can be read and modified, so passwords, secret keys and login checks there can be bypassed. Use a managed auth service such as Firebase Authentication, Supabase Auth, Auth0 or Clerk, which provides the backend for you.

Is it safe to store users and passwords in React code?

No. Everything in your JavaScript bundle is public. In my test, a minified production build still contained the email, password and secret key in plain text.

Can I create JWTs in the browser?

You can create them, but they are worthless for security, because the signing secret would have to be in the browser where anyone can copy it and sign their own tokens. JWTs must be signed and verified on a server or by an auth provider.

Is localStorage safe for storing login tokens?

It is readable by any script running on your page, so an XSS bug can steal the token. Prefer your auth provider’s session handling, keep tokens short-lived, and protect against XSS.

What is the easiest way to add login to a React app?

A managed provider. Firebase Authentication and Supabase Auth take a few lines of SDK code; Clerk provides ready-made React sign-in components; Auth0 provides a hosted login page.

Is the Firebase API key a secret?

No. It identifies your Firebase project and is meant to be in client code. Protect data with Firebase security rules and server-side token verification, not by hiding the key.

Snehasish Konger
Developed @scientyficworld.org | Technical writer @Nected | Content Developer
Connect with Snehasish Konger

On This page

Take a Pause with Intervals

A Sunday letter on building, writing, and thinking deeper as a developer — short, honest, and worth your time.

Snehasish Konger profile photo

"Hey there — I'm Snehasish. Hope this post saved you some head-scratching time! I've spent years turning technical chaos into clarity, and I'm here to be your guide through the maze of modern tech. Stick around for more lightbulb moments — we're just getting started."

Related Posts