Debugging a Midnight Login Bug in Medellín
It was just past midnight in Laureles when my coffee‑starved brain met its match: a freelance client in Tokyo couldn’t log into our staging site. Their React SPA kept flashing invalid token even after a hard refresh. Turns out my local clock drifted four minutes behind Firebase’s server time, invalidating freshly minted JWTs. One ntpdate
later the login flowed, and I walked away with a hard‑earned reminder—auth is less about passwords and more about invisible guarantees.
Why Authentication Still Trips Up Modern Front‑End Teams
Single‑page apps hand control of sessions to the browser, making tokens the new cookies. Framework‑agnostic libraries now spin up JWTs in milliseconds, OAuth providers grant one‑click federation, and Firebase Auth promises “five lines of code.” Yet junior devs regularly ship vulnerable localStorage flows or break refresh logic when React hot‑reloads. Understanding how each mechanism issues, stores, and renews credentials is crucial to building apps that feel native—whether your users are in Dominican co‑working spaces or Brazilian favelas with flaky LTE.
Toolbelt at a Glance
Tool / Concept | One‑liner purpose |
---|---|
JSON Web Token (JWT) | Self‑contained bearer token signed with HMAC or RSA. |
OAuth 2.1 | Industry spec for delegated authorization (e.g., “Log in with Google”). |
Firebase Auth | Google‑hosted user store supporting email, phone, and OAuth providers. |
CLI Command | What it does |
---|---|
npm i jsonwebtoken | Issue & verify JWTs in Node back‑ends. |
npm i passport passport-jwt | Strategy pattern for JWT in Express. |
npm i firebase | Firebase SDK (Auth, Firestore, etc.). |
npm i react-oauth/google | Thin wrapper to embed Google OAuth in React. |
Concept Foundations in Plain English
- JWT: A signed JSON blob that proves “this user is Alice until 10:42 UTC.” Because the server signs it, you can verify offline—ideal for microservices.
- OAuth: Instead of passwords, your app asks a provider (Google, GitHub) for a short‑lived authorization code, then swaps it for an access token. Users revoke access anytime.
- Firebase Auth: Google runs both the identity store and token minting; you add a client SDK and forget about refresh tokens—Firebase handles rotation.
Walkthrough 1 — Rolling Your Own JWT Flow
Server (Node + Express 18)
jsCopyEdit// authRouter.js
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import express from 'express';
const router = express.Router();
const SECRET = process.env.JWT_SECRET;
// POST /login
router.post('/login', async (req, res) => {
const user = await db.findUser(req.body.email);
if (!user || !(await bcrypt.compare(req.body.password, user.hash))) {
return res.status(401).send('Bad credentials');
}
const token = jwt.sign(
{ sub: user.id, role: 'basic' },
SECRET,
{ expiresIn: '15m' }
);
res.cookie('jwt', token, { httpOnly: true, sameSite: 'lax' });
res.json({ ok: true });
});
export default router;
Line‑by‑line
bcrypt.compare
validates the hash.jwt.sign
embeds user ID (sub
) and role;exp
set to 15 minutes.- Cookie flags (
httpOnly
,sameSite
) thwart XSS and CSRF.
Client (React)
tsxCopyEdit// useAuth.ts
import { createContext, useContext, useState } from 'react';
const AuthCtx = createContext<{loggedIn: boolean}>({ loggedIn: false });
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [loggedIn, setLoggedIn] = useState(false);
async function login(email: string, password: string) {
const res = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // send cookie
body: JSON.stringify({ email, password }),
});
if (res.ok) setLoggedIn(true);
}
return <AuthCtx.Provider value={{ loggedIn, login }}>{children}</AuthCtx.Provider>;
}
export const useAuth = () => useContext(AuthCtx);
Notes
- Cookies travel automatically; no tokens in localStorage.
credentials: 'include'
is mandatory for CORS cookies.
Walkthrough 2 — OAuth Dance with Google & react‑oauth
tsxCopyEdit// GoogleLogin.tsx
import { GoogleLogin, googleLogout } from '@react-oauth/google';
import jwtDecode from 'jwt-decode';
export default function GoogleButton() {
return (
<GoogleLogin
onSuccess={({ credential }) => {
const profile = jwtDecode(credential);
// call your back‑end to create / update user record
fetch('/oauth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken: credential }),
});
}}
onError={() => console.error('OAuth error')}
/>
);
}
Highlights
- Google sends an ID Token (JWT); you forward it to the server for verification against Google’s public keys.
- The server exchanges it for its own session JWT or sets a secure cookie.
Walkthrough 3 — Firebase Auth in Six Lines
tsxCopyEdit// firebase.ts
import { initializeApp } from 'firebase/app';
import { getAuth, signInWithEmailAndPassword } from 'firebase/auth';
const firebase = initializeApp({
apiKey: import.meta.env.VITE_FB_KEY,
authDomain: 'your-app.firebaseapp.com',
});
export const auth = getAuth(firebase);
tsxCopyEdit// LoginForm.tsx
import { auth } from './firebase';
import { signInWithEmailAndPassword } from 'firebase/auth';
export async function handleLogin(email: string, pass: string) {
await signInWithEmailAndPassword(auth, email, pass);
// Firebase sets a local indexedDB token & auto‑refreshes every hour
}
Serverless twist
- Firebase issues session cookies or JWTs you can forward to a custom back‑end with
verifyIdToken
on Cloud Functions or your own Node API.
Common Pitfalls I See Weekly
- Clock Skew Breaks JWT
Tokens are invalid if client clocks drift. Use NTP or shortenexpiresIn
and refresh more often. - Storing Tokens in localStorage
XSS can steal localStorage; prefer HTTP‑only cookies orSecureStore
on mobile. - OAuth Redirect Loop
Mis‑configured callback URL causes 302 ping‑pong. Double‑check provider dashboard and ensure state param persists through React Router navigation.
Remote‑Work Insight Box
My Bogotá client demoed at 6 p.m. EST but their London QA opened the build at 1 a.m. local. JWT expiry (set for “end of business”) had silently logged them out. We switched to refresh tokens rotated by a silent iframe. Moral: global teams need 24‑hour sessions or refresh logic that respects every time zone, not just yours.
Performance & Accessibility Checkpoints
- Bundle Size: Firebase adds ~45 kB; Auth0 React SDK adds ~12 kB. Use
vite-plugin-visualizer
to tree‑shake unused services. - Lighthouse: Audit for cookie size—large JWTs inflate headers and TTFB. Keep payloads <4 kB by omitting nested claims.
- ARIA: Attach
aria-live="polite"
to login status banners so screen readers announce “Signed in.” - Token Refresh: Use
BackgroundSync
API ornavigator.onLine
events to retry when users regain Caribbean hostel Wi‑Fi. - Secure Context: Browsers now block
navigator.credentials
onhttp://
; develop withhttps://localhost
to catch mixed‑content issues early.
Choosing the Right Flow for Your React App
Scenario | Best Choice | Rationale |
---|---|---|
Microservice API, mobile clients | JWT | Stateless verification, cheap to scale. |
Enterprise SSO, GitHub login | OAuth | Delegates password storage, meets IT policy. |
Startup MVP, no back‑end team | Firebase Auth | Serverless, email templates, phone OTP. |
I encourage juniors to prototype with Firebase in a hackathon, graduate to OAuth for social logins, and slide down into vanilla JWT when you own both client and server. The goal isn’t to pick a silver bullet—the goal is to know the trade‑offs so you’re never surprised at 4 a.m. on a balcony in Mexico City.
Key Takeaways
- React integrates smoothly with all three auth models—just mind token storage.
- JWTs excel at stateless APIs but require tight clock sync and refresh guards.
- OAuth eliminates passwords yet demands careful redirect and CSRF state handling.
- Firebase Auth offloads everything but can lock you into Google pricing and availability.
- Global teams must design sessions that survive time‑zone shifts and intermittent Wi‑Fi.
Got a thorny auth bug or a heroic fix? Share it in the comments—let’s learn together, one secure login at a time.