@bigbossstudio/strapi-plugin-password-policy
v1.1.1
Published
Strapi 5 plugin enforcing an admin password policy: minimum length + configurable complexity, expiry-based login refusal (renew via native reset), and pre-expiry reminder emails. Fully configurable and i18n-ready.
Readme
@bigbossstudio/strapi-plugin-password-policy
A Strapi 5 plugin that hardens admin-panel account passwords:
- Length + complexity — minimum length (default 15) and a configurable
"at least N of {lowercase, uppercase, digit, special}" rule, enforced on every
admin password-setting endpoint (rejected with a
ValidationErrorthe admin UI shows inline). - Rotation — once a password is older than
maxAgeDays(default 90) the plugin refuses login and directs the admin to Strapi's native "Forgot your password?" reset (which resets the clock). No email is auto-sent by the block. - Pre-expiry reminder email — a daily cron emails each admin once,
daysBefore(default 10) days before expiry, stating the exact expiry date. - Fully configurable & i18n-ready — every user-facing string can be a per-locale
map; emails are sent in each admin's
preferedLanguage.
Server-only (no admin bundle). Targets accounts in @strapi/admin, not
users-permissions API users.
Requirements
- Strapi
^5.0.0 - Node
>=18
Installation
yarn add @bigbossstudio/strapi-plugin-password-policy
# or: npm i @bigbossstudio/strapi-plugin-password-policyEnable and configure it in config/plugins.ts (Strapi finds it in node_modules
by name — no resolve needed):
export default () => ({
'password-policy': {
enabled: true,
config: {
minLength: 15,
complexity: { lowercase: true, uppercase: true, digit: true, special: true, minRequired: 3 },
maxAgeDays: 90,
baselineStrategy: 'grace', // 'grace' = existing admins start a fresh clock; 'force-immediate' = expire now
reminder: { enabled: true, daysBefore: 10 },
},
},
});Wire the two global middlewares in config/middlewares.ts, immediately after
strapi::body (so the parsed body is available, and before auth so the login
check runs):
export default [
'strapi::logger',
'strapi::errors',
'strapi::security',
'strapi::cors',
'strapi::poweredBy',
'strapi::query',
'strapi::body',
'plugin::password-policy.enforce-policy',
'plugin::password-policy.block-expired-login',
'strapi::session',
'strapi::favicon',
'strapi::public',
];Rotation state is tracked in a plugin collection type (pp_credentials); on first
boot every existing admin is seeded per baselineStrategy.
Admin password hint (optional, per project)
The native auth screens show a "min 8 characters" hint that a server-only plugin
can't change (it's a core admin translation). Override it in src/admin/app.tsx
to match your policy:
export default {
config: {
translations: {
en: { 'Auth.form.password.hint': 'Must be at least 15 characters and include at least 3 of: lowercase, uppercase, number, special character.' },
},
},
bootstrap() {},
};Configuration & i18n
Every user-facing text is overridable as a plain string or a per-locale map
({ en: '…', fr: '…' }) with {placeholder} interpolation. Locale is resolved as:
reminder email → the recipient admin's preferedLanguage; validation / login-block →
the request's Accept-Language; both fall back to defaultLocale.
'password-policy': {
enabled: true,
config: {
minLength: 15,
complexity: { lowercase: true, uppercase: true, digit: true, special: true, minRequired: 3 },
maxAgeDays: 90,
baselineStrategy: 'grace',
defaultLocale: 'en',
// Short messages. Placeholders: minLength {minLength} · byteCeiling {maxBytes}
// · complexity {minRequired}{categories} · expiredLogin {maxAgeDays}
messages: {
minLength: { en: 'Password must be at least {minLength} characters.',
fr: 'Le mot de passe doit contenir au moins {minLength} caractères.' },
complexity: { en: 'Include at least {minRequired} of: {categories}.',
fr: 'Au moins {minRequired} parmi : {categories}.' },
categoryLabels: {
lowercase: { en: 'lowercase', fr: 'minuscule' },
uppercase: { en: 'uppercase', fr: 'majuscule' },
digit: { en: 'digit', fr: 'chiffre' },
special: { en: 'special character', fr: 'caractère spécial' },
},
// byteCeiling, expiredLogin likewise…
},
reminder: {
enabled: true,
daysBefore: 10,
subject: { en: 'Your admin password is expiring soon', fr: 'Votre mot de passe expire bientôt' },
// Full control of the email body (branding, HTML, any language). Overrides the
// built-in English body. Receives the recipient locale + computed context.
render: ({ locale, expiryDateFormatted, maxAgeDays, profileUrl, user }) => ({
subject: locale === 'fr' ? 'Expiration du mot de passe' : 'Password expiry',
text: `…`,
html: `<p>…</p>`,
}),
},
},
}Anything omitted falls back to the built-in English default, so partial config is safe.
Config reference
| Key | Default | Notes |
|---|---|---|
| minLength | 15 | 1–72 (bcrypt byte ceiling) |
| complexity.{lowercase,uppercase,digit,special} | all true | which categories count |
| complexity.minRequired | 3 | must be ≤ enabled categories |
| maxAgeDays | 90 | rotation window |
| baselineStrategy | 'grace' | 'grace' | 'force-immediate' |
| defaultLocale | 'en' | fallback locale |
| messages.* | English | string | { [locale]: string } |
| reminder.enabled | true | |
| reminder.daysBefore | 10 | must be < maxAgeDays |
| reminder.subject | English | string | { [locale]: string } |
| reminder.render | – | (ctx) => { subject, text?, html? } full email override |
Development
yarn install
yarn test # jest + ts-jest
yarn build # tsc -> dist/server (+ copies content-type schema.json)prepublishOnly runs the build, so npm publish ships the compiled dist/.
License
MIT © Big Boss Studio
