Skip to main content

Building Authentication from Scratch vs Using a Boilerplate (2026)

·StarterPick Team
authenticationboilerplatesecuritynextauth2026

TL;DR

Authentication is one of the highest-leverage things a boilerplate handles. Getting auth right from scratch takes 5-10 days and requires deep security knowledge. Getting it wrong creates account takeovers, session hijacking, or credential leaks. Boilerplate auth (NextAuth, Supabase Auth, Clerk) is tested by millions of users. For 95% of SaaS products, use a boilerplate. The 5% exception: highly specific auth requirements.

Key Takeaways

  • Sessions from scratch: 2-3 days + security audit
  • OAuth (Google/GitHub): 2-3 days per provider
  • Password hashing: 1 day (easy to get wrong)
  • CSRF protection: 1 day (often forgotten)
  • Magic links: 2 days
  • 2FA/TOTP: 3-5 days
  • Total auth from scratch: 10-20 days (well done)

What "Good Auth" Actually Involves

Most developers underestimate authentication complexity. Here's the complete surface area:

Password Authentication

// The obvious part: hash and verify passwords
import { hash, compare } from 'bcrypt';

async function createUser(email: string, password: string) {
  const passwordHash = await hash(password, 12); // 12 rounds minimum
  return db.user.create({ data: { email, passwordHash } });
}

async function verifyPassword(password: string, hash: string) {
  return compare(password, hash);
}
// The non-obvious parts:
// 1. Rate limiting to prevent brute force
const rateLimiter = new RateLimiter({ max: 5, windowMs: 15 * 60 * 1000 });

// 2. Timing-safe comparison (prevents timing attacks)
import { timingSafeEqual } from 'crypto';

// 3. Password strength requirements (OWASP guidelines)
// 4. Breached password checking (HaveIBeenPwned API)
// 5. Account lockout after repeated failures
// 6. Secure password reset (time-limited tokens)
// 7. Token invalidation after use
// 8. Email enumeration prevention (same response for valid/invalid email)

Implementing all 8 of these correctly takes 3-5 days. Missing any creates vulnerabilities.


Session Management

// Sessions are more complex than JWT vs cookies
// Production session requirements:

// 1. Secure, httpOnly cookies (prevents XSS theft)
res.cookie('session', sessionToken, {
  httpOnly: true,       // Not accessible via JavaScript
  secure: true,         // HTTPS only
  sameSite: 'lax',      // CSRF protection
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// 2. Session rotation on privilege escalation
// Regenerate session ID after login to prevent session fixation
async function login(req, user) {
  const oldSessionId = req.sessionId;
  await destroySession(oldSessionId);     // Destroy old session
  const newSession = await createSession(user.id); // Create new
  return newSession.id;
}

// 3. Concurrent session handling
// 4. Session invalidation on password change
// 5. Session listing/revocation for users
// 6. Sliding vs fixed expiration

OAuth Integration

// Each OAuth provider has quirks:

// Google OAuth — token refresh, scopes, account selection
const googleProvider = {
  authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
  params: {
    access_type: 'offline',   // Get refresh token
    prompt: 'consent',        // Always show consent screen (get refresh token)
    scope: 'openid email profile',
  },
  async handleCallback(code) {
    const tokens = await exchangeCode(code);

    // Decode ID token (verify signature!)
    const payload = verifyJWT(tokens.id_token, googlePublicKeys);

    // Handle: email_verified might be false
    if (!payload.email_verified) throw new Error('Email not verified');

    return { email: payload.email, name: payload.name, id: payload.sub };
  }
};

// GitHub OAuth — different email privacy settings
async function getGitHubEmail(accessToken: string) {
  // Primary email might be private — must check /user/emails
  const emails = await fetch('https://api.github.com/user/emails', {
    headers: { Authorization: `Bearer ${accessToken}` }
  }).then(r => r.json());

  const primary = emails.find(e => e.primary && e.verified);
  if (!primary) throw new Error('No verified primary email');
  return primary.email;
}

Each provider has different quirks. NextAuth handles all of these.


What NextAuth (Used in Most Boilerplates) Gets Right

// NextAuth's session handling is correct by default:
export const authOptions: NextAuthOptions = {
  session: {
    strategy: 'jwt',          // Or 'database' for server sessions
    maxAge: 30 * 24 * 60 * 60, // 30 days
  },
  cookies: {
    sessionToken: {
      options: {
        httpOnly: true,        // XSS protection
        sameSite: 'lax',       // CSRF protection
        path: '/',
        secure: process.env.NODE_ENV === 'production',
      }
    }
  },
  callbacks: {
    async signIn({ user, account, profile }) {
      // Custom validation — return false to block sign in
      if (!user.email) return false;
      return true;
    },
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id;
        token.role = user.role;
      }
      return token;
    },
    async session({ session, token }) {
      session.user.id = token.id as string;
      session.user.role = token.role as string;
      return session;
    },
  },
};

This is correct. Building this yourself from scratch means understanding all the security decisions implicitly made here.


When to Roll Your Own Auth

Build auth from scratch when:

  1. Non-standard auth protocol: Kerberos, LDAP/AD integration, custom token formats
  2. Regulated environment: HIPAA, FedRAMP, FIPS 140-2 requirements
  3. Platform product: You're building auth as a product for others (like Auth0 or Clerk themselves)
  4. Extreme performance: Millions of auth operations per second (unlikely for most SaaS)

For standard B2B or B2C SaaS, none of these apply.


Auth Provider Comparison: NextAuth vs Clerk vs Supabase Auth

NextAuthClerkSupabase Auth
Self-hosted✅ (Supabase)
PricingFreeFree up to 10k usersFree tier
ComplexityMediumLowLow
CustomizationHighMediumMedium
Magic links
2FAVia plugin
Org/Teams❌ (manual)✅ (via RLS)
UI components✅ prebuilt

Boilerplate defaults:

  • ShipFast, T3 Stack, Epic Stack → NextAuth
  • Supastarter → Supabase Auth
  • Most commercial starters → Clerk or NextAuth

The Security Verdict

Using NextAuth, Clerk, or Supabase Auth from a reputable boilerplate:

✅ Tested by millions of users ✅ Security researchers review the code ✅ Vulnerabilities patched quickly ✅ Correct session, cookie, and CSRF handling by default

Building from scratch:

⚠️ You are the security researcher ⚠️ Vulnerabilities discovered by your users (or attackers) ⚠️ Every security decision is yours to get right

For SaaS products where auth is not a competitive differentiator, the choice is clear.


Find boilerplates with the best auth implementations on StarterPick.

Check out this boilerplate

View ShipFast on StarterPick →

Comments