How to Set Up Authentication Without Building It From Scratch
Problem
I tried to build my own authentication system from scratch. I thought, “How hard can it be? Store passwords, check passwords, done.”
Three weeks later, I had:
- Passwords stored with weak hashing (MD5, because I didn’t know better)
- No rate limiting on login endpoints (brute force city)
- Sessions that never expired (security nightmare)
- Password reset flow that didn’t work
- No email verification
- JWT implementation with no token rotation
Then I found out that 56% of web application breaches involve authentication vulnerabilities. I had built a security disaster.
What happened?
I started with a simple login form:
// This is WRONG - do not do thisconst bcrypt = require('bcrypt');
// I used weak salt roundsconst hashPassword = async (password) => { const salt = await bcrypt.genSalt(4); // Too few rounds! return await bcrypt.hash(password, salt);};
// No rate limiting, no account lockoutconst login = async (req, res) => { const { email, password } = req.body;
const user = await db.getUserByEmail(email); if (!user) { return res.status(401).json({ error: 'User not found' }); }
const match = await bcrypt.compare(password, user.password); if (match) { // No session expiration, no token rotation const token = jwt.sign({ userId: user.id }, 'my-secret-key'); return res.json({ token }); }
return res.status(401).json({ error: 'Invalid password' });};When I deployed this, I immediately ran into problems:
[Security Audit Results]CRITICAL: Password hashing uses only 4 salt rounds (should be 12+)CRITICAL: No rate limiting on login endpoint (brute force vulnerable)HIGH: JWT secret is hardcoded (should use environment variable)HIGH: No token expiration or rotationMEDIUM: No password reset flowMEDIUM: No email verificationLOW: Error messages reveal user existenceI was trying to solve problems that auth providers solved years ago.
Why This Matters
Building authentication correctly requires handling:
- Password hashing: bcrypt with 12+ rounds, or Argon2
- Session management: Secure session storage, expiration, cleanup
- Token rotation: Refresh tokens, token revocation
- Multi-factor authentication (MFA): TOTP, SMS, email codes
- Rate limiting: Prevent brute force attacks
- Password reset: Secure token generation, expiration, email delivery
- Email verification: Confirmation flows, resend logic
- OAuth integration: Google, GitHub, Facebook login
- Anomaly detection: Suspicious login detection
- Compliance: GDPR, SOC2, HIPAA requirements
Each of these is complex. Together, they’re a full-time job.
I found a Reddit discussion where experienced developers shared the same advice:
“For auth, use magic links or OAuth (Google, Facebook login) instead of storing passwords yourself.”
“Use Stripe, PayPal, or Paddle for payments. Use established auth providers for login. These teams have security as their entire job.”
“You’ve mentioned not to roll your own auth, but not what to use. My suggestion is always Auth0.”
This was the wake-up call I needed.
The Solution
I switched to established authentication providers. Here’s what I tried:
Option 1: Auth0 (Enterprise-grade)
import { Auth0Client } from '@auth0/auth0-spa-js';
const auth0 = new Auth0Client({ domain: 'your-tenant.auth0.com', client_id: 'your-client-id', redirect_uri: window.location.origin});
// Login with redirectawait auth0.loginWithRedirect();
// Check authenticationconst isAuthenticated = await auth0.isAuthenticated();const user = await auth0.getUser();
// Get token for API callsconst token = await auth0.getTokenSilently();Auth0 handled everything I was trying to build:
- Password hashing with proper algorithms
- Session management with secure expiration
- Built-in rate limiting
- MFA support out of the box
- Password reset and email verification flows
- OAuth integrations (Google, GitHub, etc.)
Option 2: Clerk (Modern, Developer-friendly)
import { ClerkProvider, SignIn, useUser } from '@clerk/nextjs';
// Wrap app with providerexport default function App({ children }) { return ( <ClerkProvider> {children} </ClerkProvider> );}
// Use in componentsfunction Profile() { const { user, isSignedIn } = useUser();
if (!isSignedIn) { return <SignIn />; }
return <div>Welcome, {user.firstName}!</div>;}Clerk is great for React/Next.js projects. I got:
- Pre-built sign-in/sign-up components
- Session management
- Organization support (multi-tenant)
- Webhooks for user events
Option 3: Supabase Auth (Open-source Alternative)
import { createClient } from '@supabase/supabase-js';import { Auth } from '@supabase/auth-ui-react';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Magic link loginawait supabase.auth.signInWithOtp({});
// OAuth loginawait supabase.auth.signInWithOAuth({ provider: 'google'});
// Auth UI componentfunction LoginPage() { return ( <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} providers={['google', 'github']} /> );}Supabase Auth integrates with PostgreSQL Row Level Security, which is powerful for authorization.
Provider Comparison
I created this comparison table to help decide:
| Provider | Best For | Free Tier | Enterprise Features |
|---|---|---|---|
| Auth0 | Enterprise apps | 7,000 MAU | SSO, MFA, Rules |
| Clerk | React/Next.js | 5,000 MAU | Organizations, Webhooks |
| Supabase | PostgreSQL apps | 50,000 MAU | RLS integration |
| Firebase | Mobile apps | 10,000 MAU | Phone auth, Anonymous |
| WorkOS | B2B SaaS | 1M MAU | SSO, SCIM, Audit logs |
Each provider has different strengths:
- Auth0: Enterprise features, extensive integrations
- Clerk: Best developer experience for React
- Supabase: Open-source, great for PostgreSQL
- Firebase: Mobile-first, generous free tier
- WorkOS: Enterprise SSO, compliance-focused
The Reason
The reason auth providers are better than custom implementations:
Security expertise: Auth providers have security teams monitoring threats 24/7. They respond to new vulnerabilities immediately. When a new attack vector is discovered, they patch it before most developers even hear about it.
Proper implementation: They use correct password hashing algorithms, implement secure session management, handle token rotation properly, and follow all security best practices.
Compliance: GDPR, SOC2, HIPAA - these require specific security controls. Auth providers handle compliance requirements that would take months to implement correctly.
Edge cases: Password reset flows, email verification, account lockout, suspicious activity detection - these are all handled.
Maintenance: Security updates, vulnerability patches, algorithm upgrades - all handled automatically.
Common Mistakes to Avoid
When I was building my own auth, I made these mistakes:
Mistake 1: Storing passwords incorrectly
// WRONG: Plain text or weak hashingconst hash = md5(password); // NEVER do this
// CORRECT: Use auth providerconst { user, error } = await supabase.auth.signUp({ email, password});// Provider handles hashing with proper algorithmMistake 2: Rolling my own JWT
// WRONG: Custom JWT implementationconst token = jwt.sign({ userId }, 'hardcoded-secret');
// CORRECT: Use provider's token systemconst token = await auth0.getTokenSilently();// Provider handles token rotation, expiration, revocationMistake 3: No rate limiting
// WRONG: No protectionapp.post('/login', loginHandler);
// CORRECT: Provider handles rate limiting// Auth providers detect and block brute force attemptsMistake 4: Building OAuth incorrectly
// WRONG: Custom OAuth without state parameterconst authUrl = `https://provider.com/auth?client_id=${id}&redirect_uri=${uri}`;
// CORRECT: Provider handles OAuth securelyawait supabase.auth.signInWithOAuth({ provider: 'google' });// Provider includes state parameter, PKCE, CSRF protectionSummary
In this post, I explained why you should never build authentication from scratch. The key point is using established providers like Auth0, Clerk, or Supabase to get secure, feature-complete authentication in minutes instead of spending months building a vulnerable custom solution.
My attempt to build auth from scratch resulted in a security disaster with weak hashing, no rate limiting, no session expiration, and missing features like password reset. Auth providers have security teams, proper implementations, compliance handling, and edge case coverage that would take individual developers months or years to replicate.
The next time you need authentication, don’t start from zero. Start with Auth0, Clerk, or Supabase.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Auth0 Documentation
- 👨💻 Clerk Authentication
- 👨💻 Supabase Auth Guide
- 👨💻 OWASP Authentication Cheat Sheet
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments