
By Vivin KV
Add two-factor authentication (2FA) to the Strapi admin panel for improved administrator security.
released April 6, 2026
npm install @vivinkv28/strapi-2fa-admin-pluginStrapi 5 plugin for OTP-based admin authentication.
This package adds the backend flow for:

The first step collects the admin email and password before starting the OTP challenge flow.

The second step shows the OTP input, resend action, and inline validation feedback during verification.
This package does not automatically replace the default Strapi admin login UI.
You need two parts:
The good part is that this npm package now includes the host patch source directly, so you can copy it into your Strapi project.
After installation, the plugin exposes:
POST /api/admin-2fa/loginPOST /api/admin-2fa/verifyPOST /api/admin-2fa/resend20.x or 22.xnpm install @vivinkv28/strapi-2fa-admin-pluginUpdate config/plugins.ts:
import type { Core } from "@strapi/strapi";
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
"admin-2fa": {
enabled: true,
config: {
otpDigits: env.int("ADMIN_OTP_DIGITS", 6),
otpTtlSeconds: env.int("ADMIN_OTP_TTL_SECONDS", 300),
maxAttempts: env.int("ADMIN_OTP_MAX_ATTEMPTS", 5),
maxResends: env.int("ADMIN_OTP_MAX_RESENDS", 3),
rateLimitWindowSeconds: env.int("ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS", 900),
loginIpLimit: env.int("ADMIN_OTP_LOGIN_IP_LIMIT", 10),
loginEmailLimit: env.int("ADMIN_OTP_LOGIN_EMAIL_LIMIT", 5),
verifyIpLimit: env.int("ADMIN_OTP_VERIFY_IP_LIMIT", 20),
verifyEmailLimit: env.int("ADMIN_OTP_VERIFY_EMAIL_LIMIT", 10),
resendIpLimit: env.int("ADMIN_OTP_RESEND_IP_LIMIT", 10),
resendEmailLimit: env.int("ADMIN_OTP_RESEND_EMAIL_LIMIT", 5),
debugTimings: env.bool(
"ADMIN_OTP_DEBUG_TIMINGS",
env("NODE_ENV", "development") !== "production"
),
},
},
});
export default config;This plugin sends OTP emails through Strapi's email plugin.
If your email provider is not configured correctly, login will fail when OTP delivery is attempted.
If your Strapi app runs behind a proxy, make sure config/server.ts is configured correctly so admin cookies work as expected.
Example:
import type { Core } from "@strapi/strapi";
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
url: env("URL", "http://localhost:1337"),
proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
app: {
keys: env.array("APP_KEYS"),
},
});
export default config;This package includes the admin UI patch source under:
host-patch/
apply-strapi-admin-2fa-patch.js
strapi-admin-2fa-patch/
services/
auth.js
auth.mjs
pages/
Auth/
components/
Login.js
Login.mjsThese are the files you can copy into your Strapi project to get the same admin OTP UI pattern.
If you want to view the same host patch files on GitHub, use:
That gives users both options:
The bundled host patch changes the Strapi admin login flow to:
POST /api/admin-2fa/loginPOST /api/admin-2fa/verifyPOST /api/admin-2fa/resendThe included login patch UI contains:
Copy these files from the package into your Strapi project:
host-patch/apply-strapi-admin-2fa-patch.js -> scripts/apply-strapi-admin-2fa-patch.jshost-patch/strapi-admin-2fa-patch/services/auth.js -> scripts/strapi-admin-2fa-patch/services/auth.jshost-patch/strapi-admin-2fa-patch/services/auth.mjs -> scripts/strapi-admin-2fa-patch/services/auth.mjshost-patch/strapi-admin-2fa-patch/pages/Auth/components/Login.js -> scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.jshost-patch/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs -> scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjsThen wire the patch script in your Strapi project's package.json:
{
"scripts": {
"prebuild": "node scripts/apply-strapi-admin-2fa-patch.js",
"predev": "node scripts/apply-strapi-admin-2fa-patch.js",
"predevelop": "node scripts/apply-strapi-admin-2fa-patch.js",
"postinstall": "node scripts/apply-strapi-admin-2fa-patch.js"
}
}POST /api/admin-2fa/login
Content-Type: application/json{
"email": "admin@example.com",
"password": "super-secret-password",
"rememberMe": true,
"deviceId": "browser-device-id"
}POST /api/admin-2fa/verify
Content-Type: application/json{
"challengeId": "***",
"code": "123456"
}POST /api/admin-2fa/resend
Content-Type: application/json{
"challengeId": "***"
}Suggested defaults:
ADMIN_OTP_DIGITS=6
ADMIN_OTP_TTL_SECONDS=300
ADMIN_OTP_MAX_ATTEMPTS=5
ADMIN_OTP_MAX_RESENDS=3
ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS=900
ADMIN_OTP_LOGIN_IP_LIMIT=10
ADMIN_OTP_LOGIN_EMAIL_LIMIT=5
ADMIN_OTP_VERIFY_IP_LIMIT=20
ADMIN_OTP_VERIFY_EMAIL_LIMIT=10
ADMIN_OTP_RESEND_IP_LIMIT=10
ADMIN_OTP_RESEND_EMAIL_LIMIT=5
ADMIN_OTP_DEBUG_TIMINGS=falseAfter setup, test:
Email OTP improves admin security, but it is still weaker than TOTP, WebAuthn, or passkeys.
Share your work with the community and get it listed in the Strapi ecosystem for everyone to discover and use.
Submit
