Skip to content
Back to Build Tutorials

Third-Party Auth

Integrate auth providers (Auth0, Clerk, NextAuth) without rolling your own session crypto.

Why

Authentication is the one feature where a self-rolled bug becomes a full account-takeover headline. A managed provider handles password hashing, session rotation, MFA, and breach detection — things that take 200+ hours to implement correctly and 5 minutes to implement insecurely. The provider also absorbs the compliance burden (SOC2, GDPR) for the auth surface, which is the part auditors scrutinize most. The cost is a per-user fee, but that fee is cheaper than one security engineer's salary and far cheaper than a breach disclosure.

When

Before the first user-facing route that needs a login. Takes 1-3 h depending on provider. Skip only for a fully public read-only app.

How

1. Pick a provider by needs, not hype
   - Clerk: fastest setup, best DX, paid from 10k MAU
   - Auth0: enterprise features, SSO, paid from 7k MAU
   - NextAuth/Auth.js: free, self-hosted, you own the DB

2. Configure the provider with redirect URLs for every env
   - localhost:3000, staging.example.com, example.com — never a wildcard
   - Store the client secret in the platform secret store, not .env

3. Wrap protected routes with a middleware/session check
   - In Next.js: middleware.ts checks the session cookie, redirects to /login if absent
   - Server components: getServerSession() — never trust client-side auth state

4. Implement role-based access with the provider's metadata, not a local DB
   - Roles live in the provider's user app_metadata; your app reads them on each request
   - This keeps roles consistent across logout/login and survives your DB being restored

5. Test the full flow: signup → email verify → login → logout → password reset
   - Use a throwaway email (Mailinator) for the e2e test in CI

Example

// middleware.ts (Next.js + Clerk)
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtected = createRouteMatcher(["/dashboard(.*)", "/api/admin(.*)"]);
export default clerkMiddleware((auth, req) => {
  if (isProtected(req)) auth().protect();
});

// Server component — read session server-side
import { auth, currentUser } from "@clerk/nextjs";
export default async function Dashboard() {
  const { userId } = auth();
  if (!userId) redirect("/login");
  const user = await currentUser();
  return <h1>Welcome {user?.firstName}</h1>;
}

Common Problems

  • ⚠Checking auth state on the client only, so a user can flip a boolean in DevTools and see the admin dashboard.
  • ⚠Using a wildcard redirect URL (localhost:*), which lets an attacker intercept the auth code on a malicious port.
  • ⚠Storing roles in a local DB that gets out of sync with the provider, so a deleted user still has access.
  • ⚠Not testing the logout flow, so sessions linger and a shared computer stays logged in for days.

Best Practices

  • ✓Always call the session check server-side — client auth state is for UI hints, never for access control.
  • ✓Rotate the provider's client secret every 90 days and store it in a secret manager, not the repo.
  • ✓Enable the provider's anomaly detection (impossible travel, brute force) — it's free and catches credential stuffing.
  • ✓Log auth events (login, logout, failed) with IP and user-agent for audit — you'll need them for any incident response.