Authentication is one of the highest-stakes decisions in any app — get it wrong and you either lock users out or expose their accounts. In 2026, the practical answer for most teams is: use a managed service or a well-maintained library, add passkeys as the primary flow for consumer apps, and keep OAuth/OIDC for B2B. Rolling custom auth is almost never worth it.
What changed in 2026
- Passkeys are mainstream. The WebAuthn/FIDO2 ecosystem matured; Apple, Google, and Microsoft credential managers sync passkeys across devices. Users expect them for consumer apps.
- Managed auth services consolidated. Clerk, Auth0 (Okta), Supabase Auth, and Firebase Auth are the dominant choices; pricing models vary significantly at scale.
- Auth.js v5 (formerly NextAuth) is framework-agnostic — works with Next.js, SvelteKit, SolidStart, and bare Node.js handlers.
- PKCE is required for all OAuth public clients (SPAs, mobile apps). Implicit flow is deprecated everywhere.
Decision framework
| Situation |
Recommended approach |
| Consumer app, new project |
Managed service (Clerk / Firebase Auth) + passkeys |
| B2B SaaS |
Managed service with SAML/OIDC SSO support (Clerk, WorkOS) |
| Next.js full-stack |
Auth.js v5 |
| Supabase project |
Supabase Auth (built-in) |
| Custom backend, full control |
Passport.js (Node) / Authlib (Python) + Argon2id passwords |
| Enterprise with existing IdP |
Integrate via SAML or OIDC, use WorkOS or Auth0 |
Option 1 — Managed service (Clerk)
npm install @clerk/nextjs
// app/layout.tsx
import { ClerkProvider } from '@clerk/nextjs';
export default function RootLayout({ children }) {
return (
<ClerkProvider>
<html><body>{children}</body></html>
</ClerkProvider>
);
}
// Protect a route
import { auth } from '@clerk/nextjs/server';
export default async function DashboardPage() {
const { userId } = await auth();
if (!userId) redirect('/sign-in');
return <Dashboard />;
}
Clerk handles passkeys, social OAuth, magic links, MFA, and session management. Pricing: free up to 10,000 MAU; ~$25/mo for 10k–100k.
Option 2 — Auth.js v5 (self-hosted sessions)
npm install next-auth@beta
// auth.ts
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
});
// app/api/auth/[...nextauth]/route.ts
export { handlers as GET, handlers as POST } from '@/auth';
Auth.js stores sessions in a DB adapter (Drizzle, Prisma, Supabase, etc.) or signed JWTs. Good choice when you want full data ownership.
Passkeys with WebAuthn
For a custom backend, use the @simplewebauthn/server + @simplewebauthn/browser pair:
import { generateRegistrationOptions, verifyRegistrationResponse } from '@simplewebauthn/server';
// Registration challenge
const options = await generateRegistrationOptions({
rpName: 'My App',
rpID: 'myapp.com',
userID: userId,
userName: userEmail,
});
// Store options.challenge in session, return options to client
The browser calls startRegistration(options) and returns a credential. Verify it server-side with verifyRegistrationResponse. On subsequent logins, use generateAuthenticationOptions and verifyAuthenticationResponse.
JWT vs sessions (2026 take)
| Dimension |
JWT (stateless) |
Sessions (stateful) |
| Revocation |
Hard — need a blocklist or short expiry |
Easy — delete from session store |
| Horizontal scale |
Trivial — no shared state |
Needs a shared store (Redis) |
| Token size |
~300–500 bytes per request |
Small cookie + server lookup |
| Complexity |
Higher (key rotation, refresh logic) |
Lower |
| Recommended for |
Stateless APIs, microservices |
Web apps with server-rendering |
See JWT vs sessions in 2026 for a deeper treatment.
How to pick
- New consumer app? → Clerk or Firebase Auth. Add passkeys as primary.
- Next.js and want data ownership? → Auth.js v5 with a DB adapter.
- Already on Supabase? → Supabase Auth. It's built in.
- B2B with enterprise SSO requirements? → WorkOS or Auth0 with SAML/OIDC.
- Custom backend, full control? → Argon2id passwords + refresh-token rotation. Budget a week.
Common mistakes
Using HS256 JWTs with a weak or hardcoded secret. Use RS256 or ES256 with generated key pairs in production.
Not revoking tokens on sign-out. Stateless JWTs keep working until expiry. Use short expiry (~15 min) + refresh tokens, or maintain a token blocklist.
Storing the JWT in localStorage. It is accessible to JavaScript and vulnerable to XSS. Use HttpOnly, Secure, SameSite=Lax cookies.
Skipping rate limits on login endpoints. An unprotected /login endpoint is a credential-stuffing target. See how to rate limit an API in 2026.
What to skip
- Custom password hashing with MD5, SHA-1, or SHA-256. Use bcrypt, scrypt, or Argon2id. Period.
- Building an OAuth server from scratch — it is a months-long project with serious security implications.
- Session tokens in query strings — they leak in server logs, referer headers, and browser history.
FAQ
Do I need MFA?
For any app with financial data, PII, or admin capabilities: yes. Managed services like Clerk make TOTP and SMS MFA a config option.
What is the difference between authentication and authorization?
Authentication answers "who are you?" Authorization answers "what are you allowed to do?" Auth is the gateway; RBAC/ABAC handles authorization separately.
Can I mix passkeys and passwords?
Yes — offer both. Many users will not have passkeys set up yet. Treat passkeys as the preferred path, passwords as fallback.
How do I handle "forgot password" flows?
Generate a time-limited, single-use token (UUID v4), store its hash in your DB, email the raw token. On submission, hash and compare. Expire after 15–30 minutes.
Where to go next