Skip to content

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:

auth.js (MY TERRIBLE ATTEMPT)
// This is WRONG - do not do this
const bcrypt = require('bcrypt');
// I used weak salt rounds
const hashPassword = async (password) => {
const salt = await bcrypt.genSalt(4); // Too few rounds!
return await bcrypt.hash(password, salt);
};
// No rate limiting, no account lockout
const 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 rotation
MEDIUM: No password reset flow
MEDIUM: No email verification
LOW: Error messages reveal user existence

I 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)

auth0-setup.js
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 redirect
await auth0.loginWithRedirect();
// Check authentication
const isAuthenticated = await auth0.isAuthenticated();
const user = await auth0.getUser();
// Get token for API calls
const 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)

clerk-setup.tsx
import { ClerkProvider, SignIn, useUser } from '@clerk/nextjs';
// Wrap app with provider
export default function App({ children }) {
return (
<ClerkProvider>
{children}
</ClerkProvider>
);
}
// Use in components
function 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)

supabase-setup.ts
import { createClient } from '@supabase/supabase-js';
import { Auth } from '@supabase/auth-ui-react';
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
// Magic link login
await supabase.auth.signInWithOtp({
});
// OAuth login
await supabase.auth.signInWithOAuth({
provider: 'google'
});
// Auth UI component
function 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:

ProviderBest ForFree TierEnterprise Features
Auth0Enterprise apps7,000 MAUSSO, MFA, Rules
ClerkReact/Next.js5,000 MAUOrganizations, Webhooks
SupabasePostgreSQL apps50,000 MAURLS integration
FirebaseMobile apps10,000 MAUPhone auth, Anonymous
WorkOSB2B SaaS1M MAUSSO, 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 hashing
const hash = md5(password); // NEVER do this
// CORRECT: Use auth provider
const { user, error } = await supabase.auth.signUp({
email,
password
});
// Provider handles hashing with proper algorithm

Mistake 2: Rolling my own JWT

// WRONG: Custom JWT implementation
const token = jwt.sign({ userId }, 'hardcoded-secret');
// CORRECT: Use provider's token system
const token = await auth0.getTokenSilently();
// Provider handles token rotation, expiration, revocation

Mistake 3: No rate limiting

// WRONG: No protection
app.post('/login', loginHandler);
// CORRECT: Provider handles rate limiting
// Auth providers detect and block brute force attempts

Mistake 4: Building OAuth incorrectly

// WRONG: Custom OAuth without state parameter
const authUrl = `https://provider.com/auth?client_id=${id}&redirect_uri=${uri}`;
// CORRECT: Provider handles OAuth securely
await supabase.auth.signInWithOAuth({ provider: 'google' });
// Provider includes state parameter, PKCE, CSRF protection

Summary

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:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments