@vivinkv28/strapi-2fa-admin-plugin 0.1.0 → 0.1.2
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/README.md +172 -94
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,49 +2,50 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
-
This
|
|
5
|
+
## What This Plugin Handles
|
|
6
6
|
|
|
7
|
-
- admin credential
|
|
7
|
+
- admin credential validation
|
|
8
8
|
- OTP challenge generation and hashing
|
|
9
9
|
- OTP resend and verification
|
|
10
10
|
- rate limiting for login, verify, and resend
|
|
11
11
|
- OTP delivery through Strapi's email plugin
|
|
12
12
|
- final Strapi admin session creation after OTP verification
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
## Important Scope
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
This package is a backend/admin-auth engine.
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
|
|
18
|
+
It does **not** replace the Strapi admin login UI by itself. Your host project still needs an admin-side integration layer that:
|
|
19
|
+
|
|
20
|
+
1. collects admin email and password
|
|
21
|
+
2. calls the plugin login endpoint
|
|
22
|
+
3. shows an OTP input UI
|
|
23
|
+
4. calls the plugin verify endpoint
|
|
24
|
+
5. optionally calls the resend endpoint
|
|
20
25
|
|
|
21
26
|
## Endpoints
|
|
22
27
|
|
|
23
|
-
The plugin exposes these
|
|
28
|
+
The plugin exposes these routes:
|
|
24
29
|
|
|
25
30
|
- `POST /api/admin-2fa/login`
|
|
26
31
|
- `POST /api/admin-2fa/verify`
|
|
27
32
|
- `POST /api/admin-2fa/resend`
|
|
28
33
|
|
|
29
|
-
See the full request and response flow in [docs/INTEGRATION.md](./docs/INTEGRATION.md).
|
|
30
|
-
|
|
31
34
|
## Requirements
|
|
32
35
|
|
|
33
36
|
- Strapi 5
|
|
34
37
|
- Node.js `20.x` or `22.x`
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
## Install In An Existing Strapi Project
|
|
38
|
+
- a configured Strapi email provider
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
Install the package in your Strapi app:
|
|
40
|
+
## Install
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
43
|
npm install @vivinkv28/strapi-2fa-admin-plugin
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
## Enable In A Strapi Project
|
|
47
|
+
|
|
48
|
+
Add the plugin to your Strapi app config:
|
|
48
49
|
|
|
49
50
|
```ts
|
|
50
51
|
// config/plugins.ts
|
|
@@ -76,73 +77,133 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin =>
|
|
|
76
77
|
export default config;
|
|
77
78
|
```
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
## Admin UI Integration
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
Because this package does not inject the full login UI by itself, the host project must integrate the admin flow.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
// config/plugins.ts
|
|
85
|
-
import type { Core } from "@strapi/strapi";
|
|
84
|
+
### Expected UI flow
|
|
86
85
|
|
|
87
|
-
|
|
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
|
-
});
|
|
86
|
+
#### 1. Credentials step
|
|
110
87
|
|
|
111
|
-
|
|
88
|
+
- collect email and password
|
|
89
|
+
- send them to `POST /api/admin-2fa/login`
|
|
90
|
+
- if successful, store `challengeId` and switch to OTP mode
|
|
91
|
+
|
|
92
|
+
#### 2. OTP step
|
|
93
|
+
|
|
94
|
+
- collect the OTP code
|
|
95
|
+
- send it to `POST /api/admin-2fa/verify`
|
|
96
|
+
- if successful, continue the normal authenticated admin flow
|
|
97
|
+
- provide a resend action that calls `POST /api/admin-2fa/resend`
|
|
98
|
+
|
|
99
|
+
### Recommended UI behavior
|
|
100
|
+
|
|
101
|
+
- keep login and OTP as separate form states
|
|
102
|
+
- do not treat password validation as a completed login
|
|
103
|
+
- complete the login only after `/verify` succeeds
|
|
104
|
+
- restart from the credentials step if the challenge expires
|
|
105
|
+
|
|
106
|
+
## Integration Guide
|
|
107
|
+
|
|
108
|
+
### Login request
|
|
109
|
+
|
|
110
|
+
```http
|
|
111
|
+
POST /api/admin-2fa/login
|
|
112
|
+
Content-Type: application/json
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Example payload:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"email": "admin@example.com",
|
|
120
|
+
"password": "super-secret-password",
|
|
121
|
+
"rememberMe": true,
|
|
122
|
+
"deviceId": "browser-device-id"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Example success response:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"data": {
|
|
131
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
|
|
132
|
+
"expiresAt": "2026-04-05T18:30:00.000Z",
|
|
133
|
+
"maskedEmail": "admin@example.com",
|
|
134
|
+
"rememberMe": true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Verify request
|
|
140
|
+
|
|
141
|
+
```http
|
|
142
|
+
POST /api/admin-2fa/verify
|
|
143
|
+
Content-Type: application/json
|
|
112
144
|
```
|
|
113
145
|
|
|
114
|
-
|
|
146
|
+
Example payload:
|
|
115
147
|
|
|
116
|
-
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
|
|
151
|
+
"code": "123456"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Example success response:
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"data": {
|
|
160
|
+
"token": "<access-token>",
|
|
161
|
+
"accessToken": "<access-token>",
|
|
162
|
+
"user": {
|
|
163
|
+
"id": 1,
|
|
164
|
+
"email": "admin@example.com"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
117
169
|
|
|
118
|
-
|
|
170
|
+
### Resend request
|
|
119
171
|
|
|
120
|
-
|
|
172
|
+
```http
|
|
173
|
+
POST /api/admin-2fa/resend
|
|
174
|
+
Content-Type: application/json
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Example payload:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
121
184
|
|
|
122
|
-
|
|
185
|
+
### UI error states to handle
|
|
123
186
|
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
128
|
-
-
|
|
187
|
+
- invalid email or password
|
|
188
|
+
- OTP expired
|
|
189
|
+
- OTP session not found
|
|
190
|
+
- invalid OTP code
|
|
191
|
+
- too many authentication attempts
|
|
192
|
+
- maximum resend attempts exceeded
|
|
129
193
|
|
|
130
|
-
|
|
194
|
+
## Host Project Requirements
|
|
131
195
|
|
|
132
|
-
|
|
196
|
+
### Email provider
|
|
133
197
|
|
|
134
|
-
|
|
135
|
-
- `/api/admin-2fa/verify`
|
|
136
|
-
- `/api/admin-2fa/resend`
|
|
198
|
+
The plugin sends OTP emails through Strapi's email plugin, so the host project must configure an email provider.
|
|
137
199
|
|
|
138
|
-
###
|
|
200
|
+
### Proxy and HTTPS
|
|
139
201
|
|
|
140
|
-
If
|
|
202
|
+
If the project runs behind a reverse proxy, configure `config/server.ts` correctly so secure admin cookies work.
|
|
141
203
|
|
|
142
|
-
|
|
204
|
+
Typical example:
|
|
143
205
|
|
|
144
206
|
```ts
|
|
145
|
-
// config/server.ts
|
|
146
207
|
import type { Core } from "@strapi/strapi";
|
|
147
208
|
|
|
148
209
|
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
|
|
@@ -160,7 +221,7 @@ export default config;
|
|
|
160
221
|
|
|
161
222
|
## Environment Variables
|
|
162
223
|
|
|
163
|
-
Suggested
|
|
224
|
+
Suggested defaults:
|
|
164
225
|
|
|
165
226
|
```env
|
|
166
227
|
ADMIN_OTP_DIGITS=6
|
|
@@ -177,51 +238,68 @@ ADMIN_OTP_RESEND_EMAIL_LIMIT=5
|
|
|
177
238
|
ADMIN_OTP_DEBUG_TIMINGS=false
|
|
178
239
|
```
|
|
179
240
|
|
|
180
|
-
##
|
|
241
|
+
## Code-Level Architecture
|
|
181
242
|
|
|
182
|
-
|
|
243
|
+
Main files:
|
|
183
244
|
|
|
184
|
-
```
|
|
185
|
-
|
|
245
|
+
```text
|
|
246
|
+
admin/src/index.js
|
|
247
|
+
server/src/index.js
|
|
248
|
+
server/src/routes/index.js
|
|
249
|
+
server/src/controllers/auth.js
|
|
250
|
+
server/src/services/auth.js
|
|
251
|
+
server/src/utils/strapi-session-auth.js
|
|
186
252
|
```
|
|
187
253
|
|
|
188
|
-
|
|
254
|
+
Responsibilities:
|
|
189
255
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
```
|
|
256
|
+
- `admin/src/index.js`
|
|
257
|
+
Minimal admin plugin stub required by the Strapi Plugin SDK.
|
|
193
258
|
|
|
194
|
-
|
|
259
|
+
- `server/src/routes/index.js`
|
|
260
|
+
Declares the login, verify, and resend routes.
|
|
195
261
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
```
|
|
262
|
+
- `server/src/controllers/auth.js`
|
|
263
|
+
Reads the request, extracts client IP, delegates to the service, and sets the admin refresh cookie after successful OTP verification.
|
|
199
264
|
|
|
200
|
-
|
|
265
|
+
- `server/src/services/auth.js`
|
|
266
|
+
Core OTP logic: credential validation, challenge lifecycle, resend/verify rules, rate limiting, email sending, and final session creation.
|
|
201
267
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
```
|
|
268
|
+
- `server/src/utils/strapi-session-auth.js`
|
|
269
|
+
Runtime helper that resolves Strapi's internal admin session utility for final session creation.
|
|
205
270
|
|
|
206
|
-
|
|
271
|
+
## Repo Docs
|
|
272
|
+
|
|
273
|
+
If you are reading the source repository directly, deeper docs are also available in:
|
|
274
|
+
|
|
275
|
+
- `docs/INTEGRATION.md`
|
|
276
|
+
- `docs/ARCHITECTURE.md`
|
|
277
|
+
|
|
278
|
+
## Development
|
|
207
279
|
|
|
208
280
|
```bash
|
|
209
|
-
npm
|
|
281
|
+
npm install
|
|
282
|
+
npm run build
|
|
210
283
|
```
|
|
211
284
|
|
|
212
|
-
|
|
285
|
+
Useful commands:
|
|
213
286
|
|
|
214
|
-
|
|
287
|
+
- `npm run build`
|
|
288
|
+
- `npm run watch`
|
|
289
|
+
- `npm run watch:link`
|
|
290
|
+
- `npm run verify`
|
|
291
|
+
|
|
292
|
+
## Publishing Checklist
|
|
215
293
|
|
|
216
|
-
1.
|
|
217
|
-
2.
|
|
218
|
-
3.
|
|
219
|
-
4.
|
|
220
|
-
5.
|
|
221
|
-
6.
|
|
294
|
+
1. run `npm install`
|
|
295
|
+
2. run `npm run build`
|
|
296
|
+
3. run `npm run verify`
|
|
297
|
+
4. verify the plugin in a real Strapi app
|
|
298
|
+
5. bump the version
|
|
299
|
+
6. publish with `npm publish --access public`
|
|
222
300
|
|
|
223
301
|
## Production Notes
|
|
224
302
|
|
|
225
|
-
- Email OTP is
|
|
226
|
-
- If
|
|
227
|
-
- For stronger security, consider
|
|
303
|
+
- Email OTP is better than password-only login, but weaker than TOTP or WebAuthn.
|
|
304
|
+
- If the admin mailbox is compromised, the second factor can still be bypassed.
|
|
305
|
+
- For stronger security later, consider TOTP, backup codes, trusted devices, or passkeys.
|