Google Authenticator 2FA in Next.js

Two-factor sign-in for a Next.js web app with any authenticator app: otplib checks the six digits in a server action, and the animated code screen frosts the page until they match.

Step-Up OTP Gate, previewLoading live demo
One payment$3

Not downloaded yet? Full refund within 14 days. Broken files get fixed or refunded. Refunds

Just this one, or all 16?

Step-Up OTP Gate$3

Just this one component.

The Shelf (All-Access)$29$54 if bought one by one

All 16 components and templates in the catalogue today, this one included. Pay once, keep them.

By buying you agree to the Terms and ask for instant access, so you lose the 14-day withdrawal right. When we still refundWe'll also email you when a new piece lands (about once a week). Unsubscribe in one click, anytime.

  • Download right after paying
  • Next.js + Cursor/v0 ready
  • Use it in client projects

By buying you agree to the Terms and ask for instant access, so you lose the 14-day withdrawal right. When we still refundWe'll also email you when a new piece lands (about once a week). Unsubscribe in one click, anytime.

Paid through Stripe. We never see your card.

Ready forCursorv0Lovableshadcn CLI

Full product pageReact · Tailwind · TypeScript

Use it in Next.js

app/billing/page.tsx

"use client";
import { useRouter } from "next/navigation";
import { StepUpGate } from "@/components/StepUpGate";
import { verifyTotp } from "./actions";

export default function BillingPage() {
  const router = useRouter();
  return (
    <StepUpGate
      title="Enter the code from your authenticator app"
      onVerify={(code) => verifyTotp(code)} // resolve true to unlock
      onVerified={() => router.refresh()}
      onCancel={() => router.push("/")}
    >
      <Billing />
    </StepUpGate>
  );
}

app/billing/actions.ts

"use server";
import { verify } from "otplib";
import { cookies } from "next/headers";

export async function verifyTotp(code: string) {
  const user = await getCurrentUser(); // your session lookup
  if (!user?.totpSecret) return false;
  const { valid } = await verify({ secret: user.totpSecret, token: code });
  if (valid) {
    // remember the step-up for 15 minutes; check this cookie (signed, in real code) before showing billing data
    (await cookies()).set("stepup", user.id, { httpOnly: true, secure: true, maxAge: 900 });
  }
  return valid;
}

app/settings/2fa/actions.ts (enrolment)

"use server";
import { generateSecret, generateURI } from "otplib";
import QRCode from "qrcode";

export async function startTotpSetup() {
  const user = await getCurrentUser();
  const secret = generateSecret();
  await savePendingSecret(user.id, secret); // keep it pending until the first code checks out
  const uri = generateURI({ issuer: "Acme", label: user.email, secret });
  return { qr: await QRCode.toDataURL(uri), secret }; // show the QR, and the secret for manual entry
}

Good to know

  • This uses otplib 13, whose API changed from version 12: import verify, generateSecret and generateURI from "otplib" (the old authenticator object is gone), and verify returns { valid }, not a boolean.
  • Everything secret stays on the server. The page only learns true or false, and the gate's arcs keep spinning until the server answers.
  • Save the secret only after the user has typed one good code during setup. Otherwise a mistyped scan locks them out.
  • Store the secret encrypted, add a rate limit on verifyTotp (5 tries per few minutes is common), and give out backup codes at setup.
  • The gate hides the page with a blur on the client. The real protection is your server refusing billing data until the step-up cookie checks out.
  • No onResend is passed, so the gate hides its Resend link. An authenticator app has nothing to resend. Not now shows a short neutral line; set cancelMessage to use your own words. Bought before October 22, 2026? Download the zip again to get both.
  • The otplib calls match the package's own README for version 13.5. The recipe hasn't been run end to end in a live app.

Install

  1. Copy from the zipPut react/src/components + react/src/data into src/, then npm i framer-motion otplib qrcode.
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

onVerify
(code) → Promise<boolean>, true unlocks
onVerified
after the unlock animation
length
number of cells, 6 by default
title / description
the dialog's text
autoSubmit
submit when the last cell fills
cancelMessage
the line Not now shows

How qodesign compares

All comparisons, with prices read on each library's own site.

Questions

Does it work with Google Authenticator, 1Password and Authy?

Yes. They all read the same otpauth:// QR code and make the same 30-second codes.

Is this React Native?

No. It's React for the web, for Next.js and other React apps in a browser.

Can Cursor or v0 add it for me?

Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.

Can I use it in client work?

Yes, in personal and client projects. You can't resell the code itself as a kit.