@vivinkv28/strapi-2fa-admin-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/admin/index.js +15 -0
- package/dist/admin/index.mjs +16 -0
- package/dist/server/index.js +44178 -0
- package/dist/server/index.mjs +44166 -0
- package/docs/ARCHITECTURE.md +203 -0
- package/docs/INTEGRATION.md +246 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# @vivinkv28/strapi-2fa-admin-plugin
|
|
2
|
+
|
|
3
|
+
`@vivinkv28/strapi-2fa-admin-plugin` is a Strapi 5 plugin that provides the backend side of an OTP-based 2FA flow for Strapi admin authentication.
|
|
4
|
+
|
|
5
|
+
This package handles:
|
|
6
|
+
|
|
7
|
+
- admin credential check
|
|
8
|
+
- OTP challenge generation and hashing
|
|
9
|
+
- OTP resend and verification
|
|
10
|
+
- rate limiting for login, verify, and resend
|
|
11
|
+
- OTP delivery through Strapi's email plugin
|
|
12
|
+
- final Strapi admin session creation after OTP verification
|
|
13
|
+
|
|
14
|
+
This package does **not** replace the Strapi admin login UI by itself. You still need a frontend/admin integration layer that calls the plugin endpoints.
|
|
15
|
+
|
|
16
|
+
## Documentation
|
|
17
|
+
|
|
18
|
+
- [Integration guide](./docs/INTEGRATION.md)
|
|
19
|
+
- [Architecture guide](./docs/ARCHITECTURE.md)
|
|
20
|
+
|
|
21
|
+
## Endpoints
|
|
22
|
+
|
|
23
|
+
The plugin exposes these content API routes:
|
|
24
|
+
|
|
25
|
+
- `POST /api/admin-2fa/login`
|
|
26
|
+
- `POST /api/admin-2fa/verify`
|
|
27
|
+
- `POST /api/admin-2fa/resend`
|
|
28
|
+
|
|
29
|
+
See the full request and response flow in [docs/INTEGRATION.md](./docs/INTEGRATION.md).
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- Strapi 5
|
|
34
|
+
- Node.js `20.x` or `22.x`
|
|
35
|
+
- A configured Strapi email provider
|
|
36
|
+
|
|
37
|
+
## Install In An Existing Strapi Project
|
|
38
|
+
|
|
39
|
+
### Option 1: Use as a published npm package
|
|
40
|
+
|
|
41
|
+
Install the package in your Strapi app:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @vivinkv28/strapi-2fa-admin-plugin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then enable it in your Strapi app plugin config:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// config/plugins.ts
|
|
51
|
+
import type { Core } from "@strapi/strapi";
|
|
52
|
+
|
|
53
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
|
|
54
|
+
"admin-2fa": {
|
|
55
|
+
enabled: true,
|
|
56
|
+
config: {
|
|
57
|
+
otpDigits: env.int("ADMIN_OTP_DIGITS", 6),
|
|
58
|
+
otpTtlSeconds: env.int("ADMIN_OTP_TTL_SECONDS", 300),
|
|
59
|
+
maxAttempts: env.int("ADMIN_OTP_MAX_ATTEMPTS", 5),
|
|
60
|
+
maxResends: env.int("ADMIN_OTP_MAX_RESENDS", 3),
|
|
61
|
+
rateLimitWindowSeconds: env.int("ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS", 900),
|
|
62
|
+
loginIpLimit: env.int("ADMIN_OTP_LOGIN_IP_LIMIT", 10),
|
|
63
|
+
loginEmailLimit: env.int("ADMIN_OTP_LOGIN_EMAIL_LIMIT", 5),
|
|
64
|
+
verifyIpLimit: env.int("ADMIN_OTP_VERIFY_IP_LIMIT", 20),
|
|
65
|
+
verifyEmailLimit: env.int("ADMIN_OTP_VERIFY_EMAIL_LIMIT", 10),
|
|
66
|
+
resendIpLimit: env.int("ADMIN_OTP_RESEND_IP_LIMIT", 10),
|
|
67
|
+
resendEmailLimit: env.int("ADMIN_OTP_RESEND_EMAIL_LIMIT", 5),
|
|
68
|
+
debugTimings: env.bool(
|
|
69
|
+
"ADMIN_OTP_DEBUG_TIMINGS",
|
|
70
|
+
env("NODE_ENV", "development") !== "production"
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export default config;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Option 2: Use as a local external plugin
|
|
80
|
+
|
|
81
|
+
If the plugin lives next to your Strapi app, point Strapi to it with `resolve`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// config/plugins.ts
|
|
85
|
+
import type { Core } from "@strapi/strapi";
|
|
86
|
+
|
|
87
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
|
|
88
|
+
"admin-2fa": {
|
|
89
|
+
enabled: true,
|
|
90
|
+
resolve: "../strapi-2fa-admin-plugin",
|
|
91
|
+
config: {
|
|
92
|
+
otpDigits: env.int("ADMIN_OTP_DIGITS", 6),
|
|
93
|
+
otpTtlSeconds: env.int("ADMIN_OTP_TTL_SECONDS", 300),
|
|
94
|
+
maxAttempts: env.int("ADMIN_OTP_MAX_ATTEMPTS", 5),
|
|
95
|
+
maxResends: env.int("ADMIN_OTP_MAX_RESENDS", 3),
|
|
96
|
+
rateLimitWindowSeconds: env.int("ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS", 900),
|
|
97
|
+
loginIpLimit: env.int("ADMIN_OTP_LOGIN_IP_LIMIT", 10),
|
|
98
|
+
loginEmailLimit: env.int("ADMIN_OTP_LOGIN_EMAIL_LIMIT", 5),
|
|
99
|
+
verifyIpLimit: env.int("ADMIN_OTP_VERIFY_IP_LIMIT", 20),
|
|
100
|
+
verifyEmailLimit: env.int("ADMIN_OTP_VERIFY_EMAIL_LIMIT", 10),
|
|
101
|
+
resendIpLimit: env.int("ADMIN_OTP_RESEND_IP_LIMIT", 10),
|
|
102
|
+
resendEmailLimit: env.int("ADMIN_OTP_RESEND_EMAIL_LIMIT", 5),
|
|
103
|
+
debugTimings: env.bool(
|
|
104
|
+
"ADMIN_OTP_DEBUG_TIMINGS",
|
|
105
|
+
env("NODE_ENV", "development") !== "production"
|
|
106
|
+
),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export default config;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Required Host App Setup
|
|
115
|
+
|
|
116
|
+
### 1. Configure an email provider
|
|
117
|
+
|
|
118
|
+
The plugin uses Strapi's `email` plugin to send OTP emails. Your host project must configure an email provider in `config/plugins.ts`.
|
|
119
|
+
|
|
120
|
+
### 2. Add an admin login integration layer
|
|
121
|
+
|
|
122
|
+
This plugin is backend-only. To use it for real admin login 2FA, your project must:
|
|
123
|
+
|
|
124
|
+
- intercept the normal admin login flow
|
|
125
|
+
- call `POST /api/admin-2fa/login`
|
|
126
|
+
- show an OTP input step
|
|
127
|
+
- call `POST /api/admin-2fa/verify`
|
|
128
|
+
- optionally call `POST /api/admin-2fa/resend`
|
|
129
|
+
|
|
130
|
+
The expected frontend flow, payloads, and response handling are documented in [docs/INTEGRATION.md](./docs/INTEGRATION.md).
|
|
131
|
+
|
|
132
|
+
If you already maintain a patched Strapi admin login screen, point it to:
|
|
133
|
+
|
|
134
|
+
- `/api/admin-2fa/login`
|
|
135
|
+
- `/api/admin-2fa/verify`
|
|
136
|
+
- `/api/admin-2fa/resend`
|
|
137
|
+
|
|
138
|
+
### 3. Ensure proxy / HTTPS settings are correct in production
|
|
139
|
+
|
|
140
|
+
If your Strapi app runs behind a proxy, make sure `config/server.ts` is configured correctly so secure admin cookies work.
|
|
141
|
+
|
|
142
|
+
Example:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
// config/server.ts
|
|
146
|
+
import type { Core } from "@strapi/strapi";
|
|
147
|
+
|
|
148
|
+
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
|
|
149
|
+
host: env("HOST", "0.0.0.0"),
|
|
150
|
+
port: env.int("PORT", 1337),
|
|
151
|
+
url: env("URL", "http://localhost:1337"),
|
|
152
|
+
proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
|
|
153
|
+
app: {
|
|
154
|
+
keys: env.array("APP_KEYS"),
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export default config;
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Environment Variables
|
|
162
|
+
|
|
163
|
+
Suggested variables for the host Strapi project:
|
|
164
|
+
|
|
165
|
+
```env
|
|
166
|
+
ADMIN_OTP_DIGITS=6
|
|
167
|
+
ADMIN_OTP_TTL_SECONDS=300
|
|
168
|
+
ADMIN_OTP_MAX_ATTEMPTS=5
|
|
169
|
+
ADMIN_OTP_MAX_RESENDS=3
|
|
170
|
+
ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS=900
|
|
171
|
+
ADMIN_OTP_LOGIN_IP_LIMIT=10
|
|
172
|
+
ADMIN_OTP_LOGIN_EMAIL_LIMIT=5
|
|
173
|
+
ADMIN_OTP_VERIFY_IP_LIMIT=20
|
|
174
|
+
ADMIN_OTP_VERIFY_EMAIL_LIMIT=10
|
|
175
|
+
ADMIN_OTP_RESEND_IP_LIMIT=10
|
|
176
|
+
ADMIN_OTP_RESEND_EMAIL_LIMIT=5
|
|
177
|
+
ADMIN_OTP_DEBUG_TIMINGS=false
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Plugin Development
|
|
181
|
+
|
|
182
|
+
Install dependencies:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Build the plugin:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
npm run build
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Watch during development:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm run watch
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Verify publishable output:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
npm run verify
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Link to a Strapi project with the SDK workflow:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
npm run watch:link
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Publishing Checklist
|
|
213
|
+
|
|
214
|
+
Before publishing:
|
|
215
|
+
|
|
216
|
+
1. Run `npm install`
|
|
217
|
+
2. Run `npm run build`
|
|
218
|
+
3. Run `npm run verify`
|
|
219
|
+
4. Confirm the host Strapi app works with the built package
|
|
220
|
+
5. Update the package version
|
|
221
|
+
6. Publish with `npm publish`
|
|
222
|
+
|
|
223
|
+
## Production Notes
|
|
224
|
+
|
|
225
|
+
- Email OTP is a practical 2FA improvement, but it is weaker than TOTP or WebAuthn.
|
|
226
|
+
- If an attacker controls the admin email inbox, they can still complete the second factor.
|
|
227
|
+
- For stronger security, consider adding trusted devices, backup codes, TOTP, or passkeys in a future version.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
function getDefaultExportFromCjs(x) {
|
|
3
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
4
|
+
}
|
|
5
|
+
var src = {
|
|
6
|
+
register() {
|
|
7
|
+
},
|
|
8
|
+
bootstrap() {
|
|
9
|
+
},
|
|
10
|
+
async registerTrads() {
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
const index = /* @__PURE__ */ getDefaultExportFromCjs(src);
|
|
15
|
+
module.exports = index;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function getDefaultExportFromCjs(x) {
|
|
2
|
+
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
|
|
3
|
+
}
|
|
4
|
+
var src = {
|
|
5
|
+
register() {
|
|
6
|
+
},
|
|
7
|
+
bootstrap() {
|
|
8
|
+
},
|
|
9
|
+
async registerTrads() {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const index = /* @__PURE__ */ getDefaultExportFromCjs(src);
|
|
14
|
+
export {
|
|
15
|
+
index as default
|
|
16
|
+
};
|