Selecting an authentication platform is a flagship case of "tech selection that's hard to redo," since once you decide, you live with it for years. The pricing model (MAU billing), B2B SSO requirements, data sovereignty, implementation speed, and the risk of vendor lock-in — these bite later but are hard to see at the PoC stage.
This article organizes the four — Cognito, Auth0, Clerk, and Supabase Auth — by evaluation axes that let decision-makers judge "which should we choose." I myself designed enterprise SSO (RS256 JWT, OIDC/SAML) with AWS Cognito in an METI-Minister's-Award-winning B2B SaaS, implemented Cognito custom authentication (Lambda triggers) in a serverless payments platform, and rolled my own stateless JWT session layer with bulk revocation via tokenVersion in a subscription platform. Based on that implementation experience, I write — not hype, but narrowed to "which to use when."
Note: pricing fluctuates wildly, and I won't assert specific amounts here. Understanding each vendor's billing model (what you're billed for) is the key to a no-regret selection. Always confirm the latest amounts on each vendor's official pricing page.
1. TL;DR — which to choose (one line each)
- AWS Cognito — if you're already AWS-centric and need enterprise SSO (SAML/OIDC federation) and fine-grained custom authentication, the first candidate. The DX is rough, but data sovereignty, cost efficiency, and integration with AWS services are powerful.
- Auth0 — best if your center of gravity is a B2B multi-tenant SaaS with requirements like "federate with a different IdP per customer company" or "manage by Organizations." The most mature feature-wise, hence premium pricing.
- Clerk — overwhelming if you want to launch B2C/startups fastest on Next.js. The developer experience (DX) of pre-built UI and middleware is in a class of its own. Cost tends to rise as MAU grows.
- Supabase Auth — the natural choice if you already use Supabase (Postgres), or want to tightly couple authentication with the DB's Row Level Security (RLS). OSS and self-hostable, with good cost efficiency too.
When unsure, start from these questions — "is this B2B or B2C," "where must the data live," and "which cloud/DB are you already on." These three almost narrow it down.
2. Evaluation axes — what to look at to decide
Comparing authentication platforms by "number of features" fails. What to look at is the following seven axes.
- Cost (MAU billing) — all four are basically billed by MAU (Monthly Active User). The issue is "what counts as an MAU" and "are higher-end features like SSO/MFA billed separately." Supabase typically bills SSO MAU and third-party MAU separately, in addition to regular MAU. Cognito tiers by monthly active users, and threat protection (Advanced Security / Threat Protection) is a separate tier. For B2C where user count grows linearly vs. B2B with few but high-value users, the optimal answers invert.
- B2B SSO / SAML / OIDC — is it a world where customer companies say "let us log in with our own Okta or Entra ID (formerly Azure AD)"? Here Auth0 and Cognito are thick, while Clerk and Supabase handle it via higher plans/add-ons.
- Self-hosting / data sovereignty — must user credentials live within your own/your country's infrastructure (finance, healthcare, public sector)? Supabase is OSS and fully self-hostable; Cognito is a managed service within your own AWS account. Auth0/Clerk are basically SaaS (Auth0 has higher forms like Private Cloud).
- DX (implementation speed) — speed to launch. Clerk is a head above, Supabase follows, Auth0 has a learning cost given its many features, and Cognito needs the most "assembling."
- Custom authentication — requirements off the standard flow (proprietary multi-stage auth, card PIN, legacy DB matching, etc.). Cognito's Lambda triggers and Auth0's Actions are representative.
- Vendor lock-in — ease of migration. Standards-compliant (OIDC), or deeply dependent on proprietary APIs.
- Compliance — SOC 2 / ISO 27001 / HIPAA / PCI DSS, etc. Cognito complies with SOC 1–3, PCI DSS, and ISO 27001, and is HIPAA-BAA eligible (the cloud security responsibility boundary).
3. Comparison table of the four + α
| Aspect | AWS Cognito | Auth0 | Clerk | Supabase Auth |
|---|---|---|---|---|
| Main battlefield | AWS-native / enterprise CIAM | B2B enterprise / mature SaaS | Next.js B2C / startups | Postgres apps / RLS tight coupling |
| Billing model | MAU tiered + threat protection separate tier | MAU tiers (separate B2C/B2B plans, upper end negotiated) | Free MAU + usage, B2B/SAML add-on | MAU + distinct SSO MAU, etc., free tier |
| SAML/OIDC federation | Standard support (can be IdP or SP) | The thickest (Enterprise Connections) | Higher-plan add-on | SAML on Pro+, SSO MAU billing |
| Self-hosting | Managed within your AWS | Basically SaaS (higher forms exist) | SaaS only | OSS, fully self-hostable |
| Custom auth | Free with Lambda triggers | Extend with Actions | Limited (purpose-specific) | Extend with DB/Edge Functions |
| Next.js DX | Average (needs assembly) | Good | Best (dedicated middleware) | Good (rich SSR helpers) |
| DB/RLS integration | None (design separately) | None | None | Yes (one with Postgres RLS) |
| Tokens | RS256 JWT (ID/access/refresh) | JWT | JWT | JWT (access/refresh) |
| Lock-in degree | Mid (OIDC-compliant but AWS-dependent) | Mid-to-high (easy to depend on features) | Mid-to-high (UI/SDK dependence) | Low-to-mid (OSS, Postgres standard) |
The self-hosted / roll-your-own option (and why to avoid it)
- Keycloak / Ory — OSS IdP/authorization platforms. Strong for organizations where data sovereignty is an absolute requirement and there's an ops team. But you bear the operational responsibility for availability, patching, and scaling yourself, so the TCO (total cost of ownership) often ends up higher than managed.
- Full roll-your-own auth — as a rule, I don't recommend it. Password hashing, token signing/verification, refresh rotation, session revocation, account-enumeration countermeasures, rate limiting, MFA, audit logs… get any one of these wrong and it's immediately a security incident. Authentication is an area where "reinventing the wheel" turns directly into a vulnerability. As described below, I did roll my own JWT session layer, but I limited it to the "session/revocation control layer," not the "user directory," and left mature parts like signature verification to libraries. If you roll your own, the iron rule is to cut the responsibility to the minimum.
4. Provider deep dives
4.1 AWS Cognito — enterprise & AWS-native
Strengths. A User Pool is a self-contained user directory and an OIDC provider, and it issues unified JWTs (RS256-signed ID/access/refresh tokens) for both local users and federated users. It can federate via SAML 2.0/OIDC with workforce IdPs like Okta, ADFS, and Entra ID, where Cognito acts as the SP (service provider), receives external claims, and normalizes them into its own token format — i.e., the app only needs to understand "one kind of Cognito token." This pays off for enterprise SSO. MFA supports TOTP and SMS. An Identity Pool integrates with AWS STS and can issue temporary credentials to S3 or DynamoDB for authenticated users — handling authorization to AWS resources end-to-end is Cognito's unique strength.
Weaknesses. The hosted UI (Managed Login) has quirks in customization freedom and isn't suited to elaborate designs. The console experience and documentation flow lack the "working in minutes" polish of Clerk. There are operational landmines too, like the difficulty of changing the attribute schema.
Ideal scenario. When you're already AWS-centric, with enterprise SSO requirements, authorization federation to AWS resources, and a need for custom authentication.
Custom authentication (Lambda triggers). This is where Cognito's true value lies. With the three triggers DefineAuthChallenge / CreateAuthChallenge / VerifyAuthChallengeResponse, you can build multi-stage flows not in the standard. What I implemented in a payments platform was exactly this trigger-based card-PIN authentication. The UserMigration trigger for lazily migrating legacy-DB users into Cognito is practical too (described below).
4.2 Auth0 — the royal road of B2B enterprise
Strengths. Its Organizations feature is excellent, head-on supporting B2B multi-tenancy — "provide each customer company its own branding and federated login." Enterprise Connections (SAML, OIDC, Entra ID, etc.) where a customer brings their own IdP is the thickest, and it has machine-to-machine (M2M) access to B2B APIs and an Organizations API for customers to self-manage their own organizations. Extension is done with Actions (serverless functions injected at each point of the auth flow), and Universal Login provides a consistent login experience.
Weaknesses. Given the feature maturity, it tends to be premium-priced. The availability of Organizations and enterprise features depends on plan or individual contract, and the upper end is negotiated. For small-scale B2C it tends to be overspec and cost-excessive.
Ideal scenario. A world where customers are large enterprises and "let us log in with our own IdP" is a given — a B2B SaaS needing per-tenant management, billing, and branding.
4.3 Clerk — the king of Next.js DX
Strengths. Its integration with Next.js is overwhelming. You can write route protection declaratively with clerkMiddleware(), and with pre-built UI like <SignIn />, <UserButton />, and <OrganizationSwitcher />, you can take auth screens to production quality in minutes. It also has Organizations (B2B), session management, MFA, bot protection, and Webhooks, and even provides B2C/B2B billing features.
Weaknesses. Its dependence on the UI/SDK is deep, and moving to another platform later is correspondingly large work. Pricing tends to rise as MAU grows, and B2B SAML SSO and the like are upper-tier/add-on. The data is on Clerk's side, so it doesn't fit data-sovereignty requirements.
Ideal scenario. When you want to launch a B2C product or startup on Next.js in the shortest path. It best grants "don't burn out on auth; focus on the product."
4.4 Supabase Auth — synergy with Postgres + RLS
Strengths. Users are stored in Postgres's auth.users, and the issued JWT's claims (sub, etc.) can be referenced directly in Row Level Security (RLS) policies. That is, "authentication" and "row-level authorization" become continuous within the same DB. With RLS using auth.uid(), tenant separation and per-user access control are achieved without writing app code. It supports password, Magic Link, OTP, 19+ social providers, and arbitrary OAuth2/OIDC provider federation. Being OSS and fully self-hostable is big for data sovereignty.
Weaknesses. SAML SSO is SSO-MAU-billed on Pro+. It doesn't yet have the enterprise-integration thickness of Cognito/Auth0, so if large-scale workforce SSO is your main battlefield, design ingenuity is needed.
Ideal scenario. When you already use Supabase/Postgres, want to complete authorization with RLS, want to keep cost down, and want to leave a self-hosting escape route for the future.
5. Implementation snippets
5.1 A provider-agnostic JWT-verification gate (Next.js 16 middleware)
Whatever platform you use, "trust only JWTs whose signature you've verified" is the common iron rule. Reading claims without verifying the signature is the same as accepting tampering. Here's a minimal gate that verifies the signature against JWKS (the public key set) with jose. It assumes RS256 (Cognito, etc.), fetching and caching the public key from the issuer's JWKS endpoint.
// lib/auth/verify.ts
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
// 環境変数で発行者を切り替え(プロバイダ非依存)。
// Cognito: https://cognito-idp.<region>.amazonaws.com/<userPoolId>
const ISSUER = process.env.AUTH_ISSUER_URL!;
const AUDIENCE = process.env.AUTH_AUDIENCE; // Cognitoのaccess tokenはaud不在のためclient_idで別途検証
// JWKSはモジュールスコープでキャッシュ(リクエスト毎に再取得しない)。
const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/.well-known/jwks.json`));
export async function verifyToken(token: string): Promise<JWTPayload> {
// 署名・iss・exp・nbfを検証。失敗すれば例外。決して握りつぶさない。
const { payload } = await jwtVerify(token, JWKS, {
issuer: ISSUER,
audience: AUDIENCE,
algorithms: ["RS256"], // alg混同攻撃を防ぐため許可アルゴリズムを固定
});
return payload;
}
// proxy.ts(Next.js 16のミドルウェア。15以前は middleware.ts)
import { NextResponse, type NextRequest } from "next/server";
import { verifyToken } from "@/lib/auth/verify";
const PROTECTED = [/^\/app(?:\/|$)/, /^\/api\/(?!auth\/)/];
export async function proxy(req: NextRequest) {
const needsAuth = PROTECTED.some((re) => re.test(req.nextUrl.pathname));
if (!needsAuth) return NextResponse.next();
// トークンはhttpOnly Cookieから読む(localStorageは使わない。後述)。
const token = req.cookies.get("access_token")?.value;
if (!token) return redirectToLogin(req);
try {
const claims = await verifyToken(token);
// 検証済みクレームを下流へ。生トークンは渡さない。
const headers = new Headers(req.headers);
headers.set("x-user-id", String(claims.sub));
return NextResponse.next({ request: { headers } });
} catch {
// 検証失敗=改ざん/失効/期限切れ。一律でログインへ。
return redirectToLogin(req);
}
}
function redirectToLogin(req: NextRequest) {
const url = new URL("/login", req.url);
url.searchParams.set("next", req.nextUrl.pathname);
return NextResponse.redirect(url);
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
There are three points. (1) Cache the JWKS (don't fetch the public key on every request). (2) Fix algorithms to block alg: none and HS256-confusion attacks. (3) Always fall to login on a verification exception — "let it through for now" is the worst.
5.2 A provider-specific example: Clerk (Next.js)
Riding the official conventions, the platform takes on the verification logic. With Clerk, you can write it declaratively like this.
// proxy.ts
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
const isProtected = createRouteMatcher(["/app(.*)", "/api(.*)"]);
export default clerkMiddleware(async (auth, req) => {
// protect()は未認証なら自動でサインインへリダイレクト。
if (isProtected(req)) await auth.protect();
});
export const config = {
matcher: [
"/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|png|jpe?g|svg|woff2?)).*)",
"/(api|trpc)(.*)",
],
};
The contrast with the hand-built 5.1 is obvious. 5.2 if speed is justice for B2C/Next.js; 5.1 if you want to hold the abstraction boundary yourself for data sovereignty or multi-provider support — this split itself mirrors the selection.
6. The reality of migration
"How to move existing users" is the point buyers most want to know but is rarely discussed. If you have 100,000 users, you always hit the wall that password hashes can't be moved / shouldn't be moved. Hashes like bcrypt are one-way, and no one holds the plaintext.
The realistic strategies are these three.
(1) Lazy migration is the main play. Prepare an empty directory in the new platform, and the flow is: the moment a user next logs in, match against the old system → on success, create the user in the new platform. With Cognito, the UserMigration Lambda trigger is exactly for this purpose.
// Cognito UserMigration トリガー(Lazy migration)
import type { UserMigrationTriggerEvent } from "aws-lambda";
import { verifyAgainstLegacy } from "./legacy"; // 旧DBへの照合(外部入力として厳格に扱う)
export async function handler(
event: UserMigrationTriggerEvent,
): Promise<UserMigrationTriggerEvent> {
if (event.triggerSource === "UserMigration_Authentication") {
// 旧システムでパスワードを検証。ここで初めて平文を扱える。
const user = await verifyAgainstLegacy(event.userName, event.request.password);
if (!user) throw new Error("Bad credentials"); // 認証失敗は曖昧なメッセージで
// 検証できたら属性をCognitoへ移植。以後はCognitoが正となる。
event.response.userAttributes = {
email: user.email,
email_verified: "true",
};
event.response.finalUserStatus = "CONFIRMED";
event.response.messageAction = "SUPPRESS"; // 「ようこそ」メールを抑止
}
return event;
}
(2) Dual-run (parallel operation) to avoid downtime. A big-bang cutover causes incidents. Run both for a period — reads from the new platform, writes to both — and wind down the old system while monitoring the migration rate with metrics. When I designed enterprise SSO with Cognito, I adopted a design that kept downtime at zero by routing the federation path first and lazily moving local users.
(3) When you must move the hashes themselves. Some, like Auth0, support bulk import of bcrypt hashes (must be the same algorithm and cost). If usable, you can choose a one-shot migration, but always confirm the constraints on supported algorithms.
What you truly must protect in migration is "don't force users to reset their passwords." That's the biggest cause of churn. Lazy migration is the only realistic answer that migrates without breaking this experience.
7. Security essentials
You're not safe just by "choosing" an authentication platform. It's decided by the implementation details.
- Where to store tokens: httpOnly cookies, full stop. Putting access/refresh tokens in
localStoragemeans one XSS pulls them all. The basic is to put them in httpOnly + Secure + SameSite cookies, unreadable from JS. The BFF pattern (the browser holds only cookies; tokens are handled server-side) is even more solid. - Refresh-token rotation. Issue a new refresh token on each refresh and invalidate the old one (rotation). Plus reuse detection — if a once-used old token is re-presented, judge it "stolen" and revoke the entire family.
- Session revocation (the tokenVersion pattern). The weakness of stateless JWTs is "immediate revocation after issuance is hard." What I adopted in a subscription platform is holding a
tokenVersioninteger on the user record and embedding the same value in the JWT. IncrementtokenVersionon password change, fraud detection, or bulk logout, and all existing tokens are invalidated by version mismatch at verification. With one number in the DB, "immediate logout on all devices" takes effect — a pattern that pays off operationally.
// tokenVersionによる一括失効(検証側)
async function assertNotRevoked(claims: { sub: string; tokenVersion: number }) {
const current = await getUserTokenVersion(claims.sub); // DBの現在値
if (claims.tokenVersion !== current) {
throw new Error("Session revoked"); // 不一致=失効済み。即拒否。
}
}
- Account-enumeration prevention. Don't distinguish between "that email isn't registered" and "wrong password." On sign-up and password reset too, don't leak existence via response time or message (reset always returns "we sent it"). Always sign OTPs with HMAC-SHA256, give them an expiry, and add an attempt limit — exactly as I implemented in the subscription platform's OTP (HMAC-SHA256).
- Common principles. Validate all external input (IdP claims, redirect URLs, state). In OIDC, don't omit PKCE and state/nonce verification. Don't swallow errors, and fall auth failures uniformly to vague messages.
8. FAQ
Q. In the end, what should I ask first? A. Three things: "is this B2B or B2C," "where must the data live," and "which cloud/DB are you already on." B2B enterprise + external-IdP federation → Auth0 or Cognito; Next.js B2C with a speed focus → Clerk; Postgres/RLS premise → Supabase — it's almost decided here.
Q. Is roll-your-own really a no-go? A. Rolling your own entire user directory and token signing/verification isn't recommended. It's a breeding ground for vulnerabilities. But thinly rolling your own "control layer" like session revocation and OTP on top of mature libraries is realistic, and I do so. The condition is cutting the responsibility to the minimum.
Q. I can't predict my budget with MAU billing. How do I estimate? A. First confirm "the definition of active" and "whether higher-end features (SSO/MFA/Organizations) are billed separately." For B2C, user count grows linearly so the MAU unit price matters; conversely for B2B, "the unit price of tenant/SSO features" matters more than user count. Also watch for free-tier cliffs (sudden tiered billing).
Q. I hear Cognito has low design freedom. A. The hosted UI does have quirks. It's a choice of either accepting and using the hosted UI, or hitting Cognito's user-pool APIs directly to build your own UI. The latter is free, but you bear the responsibility for the security implementation yourself.
Q. If it's SAML/OIDC-compliant, is switching later easy? A. The federation part (external-IdP integration) is relatively movable if OIDC-compliant, while the deeper the dependence on the user directory, passwords, and proprietary flows, the heavier the migration. That's exactly why §6's lazy migration pays off. Regard standards compliance as "an investment that lowers lock-in."
Q. Can you really build an authentication platform this far with one person + generative AI? A. Yes. I accelerate development with one-person × generative AI (Claude Code) while always placing human verification gates on critical areas like authentication. Write tests for signature verification, revocation, and enumeration countermeasures first, and bind the AI-written code with those tests. Speed and safety aren't a trade-off; they're reconciled by verifiability.
9. Get in touch
Selecting, implementing, and migrating an authentication platform is "tech selection you can't redo." I've designed and implemented enterprise SSO with Cognito (RS256 JWT, OIDC/SAML) in an METI-Minister's-Award-winning B2B SaaS, Cognito custom authentication (Lambda triggers) in a payments platform, and a JWT session layer with tokenVersion bulk revocation in a subscription platform.
- Selection: from B2B/B2C, data sovereignty, cost, and your existing stack, I present the optimal platform in a comparison report.
- Implementation: secure token operations (httpOnly cookies, rotation, revocation), SSO/MFA, and RLS integration, at production quality.
- Migration: with lazy migration and dual-run, I design a migration that doesn't force users to reset passwords and produces no downtime.
With one-person × generative AI (Claude Code) — fast, cheap, yet safe by passing through verification gates. If you're stuck on anything around authentication, feel free to reach out.