Skip to main content
Back to Blog
Architecture

Authentication Is Harder Than You Think

December 28, 202511 min read
AuthenticationSecuritySupabaseJWTArchitecture

Authentication Is Harder Than You Think

Every project plan I've ever written has a line item: "Authentication — 2 days."

Every project retrospective has a note: "Auth took 2 weeks."

I've built auth systems 4 times now. Each time, I underestimate it. Here's why, and what I finally learned.

The Iceberg

What you think auth is:

  • Login form
  • Store a token
  • Check if token is valid
  • Done

What auth actually is:

  • Login form (email/password + OAuth + magic links + MFA?)
  • Password hashing (bcrypt, argon2, what cost factor?)
  • Session management (JWT vs session cookie vs both?)
  • Token refresh (silent refresh, rotation, revocation)
  • CSRF protection (same-site cookies, double-submit token)
  • Rate limiting (on login, on registration, on password reset)
  • Password reset flow (token generation, expiry, single-use)
  • Email verification (token, resend logic, what if they change email?)
  • Account lockout (how many attempts? What's the unlock flow?)
  • Role-based access (admin vs user vs moderator)
  • API key management (for programmatic access)
  • Session invalidation on password change
  • "Remember me" vs "this session only"
  • Login from new device notification
  • Audit logging (who logged in, when, from where)

That's 15+ features. At 1-2 days each, you're looking at a month.

What I Do Now: Use Supabase Auth and Extend

After building custom auth twice and hating my life both times, I now start with Supabase Auth (or Clerk, or Auth.js). It handles:

  • Email/password with bcrypt
  • OAuth providers (Google, GitHub, Discord)
  • JWT tokens with refresh
  • Email verification
  • Password reset
  • Session management
  • Rate limiting

That's 80% of auth, handled by people who think about auth full-time. I focus on the 20% that's specific to my app:

```typescript // My auth layer is thin — it extends Supabase Auth with app-specific logic async function handleLogin(email: string, password: string) { // Supabase handles the actual authentication const { data, error } = await supabase.auth.signInWithPassword({ email, password });

if (error) { // My addition: audit logging await logAuthEvent('login_failed', { email, reason: error.message }); throw error; }

// My addition: check subscription status const subscription = await getActiveSubscription(data.user.id); if (!subscription) { // Redirect to pricing, not error return { user: data.user, redirect: '/pricing' }; }

// My addition: update last login await db.profiles.update({ where: { userId: data.user.id }, data: { lastLoginAt: new Date(), loginCount: { increment: 1 } } });

await logAuthEvent('login_success', { userId: data.user.id }); return { user: data.user, subscription }; } ```

The Mistakes I Made (So You Don't Have To)

Mistake 1: JWT tokens in localStorage. localStorage is accessible to any JavaScript on the page. If you have an XSS vulnerability, the attacker gets your auth token. Use httpOnly cookies instead.

Mistake 2: Not rotating refresh tokens. If a refresh token is stolen, the attacker has access forever. Rotate refresh tokens on every use — when a refresh token is used, issue a new one and invalidate the old one.

Mistake 3: Forgetting to invalidate sessions on password change. If a user changes their password because they think they've been compromised, all existing sessions should be killed. I shipped without this once. It defeats the purpose of changing your password.

Mistake 4: Password reset tokens that don't expire. My first implementation had reset tokens that were valid forever. That means if someone's email is compromised a year from now, old reset links still work. Set a 1-hour expiry.

The Decision Framework

Situation Use
Building an MVP Supabase Auth / Clerk (don't build auth)
SaaS with subscriptions Supabase Auth + custom subscription logic
Enterprise with SSO Auth0 or WorkOS (don't even try to build SAML)
API-only service API keys + rate limiting (simpler than JWT)
Never Rolling your own crypto, password hashing, or token generation

The Bottom Line

Auth is infrastructure, not a feature. Users don't care about your auth — they care about getting into the app. Spend as little time as possible on auth mechanics and as much time as possible on what happens after login.

The best auth system is one you didn't build.

Want to see this in action?

Check out the projects and case studies behind these articles.