@vivinkv28/strapi-2fa-admin-plugin 0.1.5 → 0.1.7

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.
Files changed (2) hide show
  1. package/README.md +314 -157
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,31 +1,36 @@
1
1
  # @vivinkv28/strapi-2fa-admin-plugin
2
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.
3
+ `@vivinkv28/strapi-2fa-admin-plugin` is a Strapi 5 plugin that adds the backend side of an OTP-based admin login flow.
4
4
 
5
- ## What This Plugin Handles
5
+ It gives your Strapi project:
6
6
 
7
- - admin credential validation
8
- - OTP challenge generation and hashing
7
+ - admin credential validation before OTP
8
+ - OTP challenge creation
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
- - final Strapi admin session creation after OTP verification
12
+ - final Strapi admin session creation only after OTP verification
13
13
 
14
- ## Important Scope
14
+ ## What This Package Is
15
15
 
16
- This package is a backend/admin-auth engine.
16
+ This package is a **backend/admin-auth plugin**.
17
17
 
18
- It does **not** replace the Strapi admin login UI by itself. Your host project still needs an admin-side integration layer that:
18
+ It does **not** automatically replace the default Strapi admin login UI.
19
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
+ To use it in a real project, you need two parts:
21
+
22
+ 1. this plugin for the backend OTP/auth logic
23
+ 2. an admin login screen integration in your Strapi project that calls the plugin endpoints
24
+
25
+ That means this package is ideal if you want:
26
+
27
+ - a reusable backend OTP engine
28
+ - control over how the admin login UI looks
29
+ - a project-level patch/customization for the Strapi admin login page
25
30
 
26
31
  ## Endpoints
27
32
 
28
- The plugin exposes these routes:
33
+ After installation, the plugin exposes:
29
34
 
30
35
  - `POST /api/admin-2fa/login`
31
36
  - `POST /api/admin-2fa/verify`
@@ -43,12 +48,11 @@ The plugin exposes these routes:
43
48
  npm install @vivinkv28/strapi-2fa-admin-plugin
44
49
  ```
45
50
 
46
- ## Enable In A Strapi Project
51
+ ## Step 1: Enable The Plugin
47
52
 
48
- Add the plugin to your Strapi app config:
53
+ In your Strapi project, update `config/plugins.ts`:
49
54
 
50
55
  ```ts
51
- // config/plugins.ts
52
56
  import type { Core } from "@strapi/strapi";
53
57
 
54
58
  const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
@@ -77,42 +81,296 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin =>
77
81
  export default config;
78
82
  ```
79
83
 
80
- ## Admin UI Integration
84
+ ## Step 2: Configure Email
85
+
86
+ This plugin sends OTP emails through Strapi's email plugin.
87
+
88
+ Your project must have a working email provider configuration in `config/plugins.ts`.
89
+
90
+ If email is not configured, login will fail when OTP delivery is attempted.
91
+
92
+ ## Step 3: Make Sure Server/Proxy Settings Are Correct
93
+
94
+ If your Strapi app runs behind a proxy, configure `config/server.ts` correctly so admin cookies work as expected.
95
+
96
+ Typical example:
97
+
98
+ ```ts
99
+ import type { Core } from "@strapi/strapi";
100
+
101
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
102
+ host: env("HOST", "0.0.0.0"),
103
+ port: env.int("PORT", 1337),
104
+ url: env("URL", "http://localhost:1337"),
105
+ proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
106
+ app: {
107
+ keys: env.array("APP_KEYS"),
108
+ },
109
+ });
110
+
111
+ export default config;
112
+ ```
113
+
114
+ ## Step 4: Add Admin Login UI Integration
115
+
116
+ This package does not inject the login UI automatically.
117
+
118
+ Your Strapi project must customize the admin login flow so it works like this:
119
+
120
+ 1. admin enters email and password
121
+ 2. frontend calls `POST /api/admin-2fa/login`
122
+ 3. frontend shows OTP screen
123
+ 4. admin enters OTP
124
+ 5. frontend calls `POST /api/admin-2fa/verify`
125
+ 6. optional resend button calls `POST /api/admin-2fa/resend`
126
+
127
+ ## Recommended Project Structure For The Admin Patch
128
+
129
+ In your Strapi project, keep your admin patch files in your own `scripts` folder:
130
+
131
+ ```text
132
+ your-project/
133
+ scripts/
134
+ strapi-admin-2fa-patch/
135
+ services/
136
+ auth.js
137
+ auth.mjs
138
+ pages/
139
+ Auth/
140
+ components/
141
+ Login.js
142
+ Login.mjs
143
+ apply-strapi-admin-2fa-patch.js
144
+ ```
145
+
146
+ This keeps your admin customizations reproducible and easy to reapply after `npm install`.
147
+
148
+ ## Step 5: Patch The Strapi Admin Auth Service
149
+
150
+ Create these files in your Strapi project:
151
+
152
+ - `scripts/strapi-admin-2fa-patch/services/auth.js`
153
+ - `scripts/strapi-admin-2fa-patch/services/auth.mjs`
154
+
155
+ Start from the corresponding Strapi admin auth service file and add the OTP mutations below.
156
+
157
+ ### Add these mutations
158
+
159
+ ```js
160
+ adminLoginWithOtp: builder.mutation({
161
+ query: (body) => ({
162
+ method: 'POST',
163
+ url: '/api/admin-2fa/login',
164
+ data: body,
165
+ }),
166
+ transformResponse(res) {
167
+ return res.data;
168
+ },
169
+ }),
170
+
171
+ verifyAdminLoginOtp: builder.mutation({
172
+ query: (body) => ({
173
+ method: 'POST',
174
+ url: '/api/admin-2fa/verify',
175
+ data: body,
176
+ }),
177
+ transformResponse(res) {
178
+ return res.data;
179
+ },
180
+ invalidatesTags: ['Me'],
181
+ }),
182
+
183
+ resendAdminLoginOtp: builder.mutation({
184
+ query: (body) => ({
185
+ method: 'POST',
186
+ url: '/api/admin-2fa/resend',
187
+ data: body,
188
+ }),
189
+ transformResponse(res) {
190
+ return res.data;
191
+ },
192
+ }),
193
+ ```
194
+
195
+ ### Export these hooks
81
196
 
82
- Because this package does not inject the full login UI by itself, the host project must integrate the admin flow.
197
+ ```js
198
+ const {
199
+ useAdminLoginWithOtpMutation,
200
+ useVerifyAdminLoginOtpMutation,
201
+ useResendAdminLoginOtpMutation,
202
+ } = authService;
203
+ ```
204
+
205
+ ## Step 6: Patch The Strapi Login Screen
206
+
207
+ Create these files in your Strapi project:
83
208
 
84
- ### Expected UI flow
209
+ - `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
210
+ - `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
85
211
 
86
- #### 1. Credentials step
212
+ This component must replace the normal one-step login with a two-step state:
87
213
 
88
- - collect email and password
89
- - send them to `POST /api/admin-2fa/login`
90
- - if successful, store `challengeId` and switch to OTP mode
214
+ - credentials step
215
+ - OTP step
91
216
 
92
- #### 2. OTP step
217
+ ### Minimum state you need
93
218
 
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`
219
+ ```js
220
+ const [otpStep, setOtpStep] = React.useState(null);
221
+ const [apiError, setApiError] = React.useState();
222
+
223
+ const [adminLoginWithOtp, { isLoading: isLoggingIn }] = useAdminLoginWithOtpMutation();
224
+ const [verifyAdminLoginOtp, { isLoading: isVerifyingOtp }] = useVerifyAdminLoginOtpMutation();
225
+ const [resendAdminLoginOtp, { isLoading: isResendingOtp }] = useResendAdminLoginOtpMutation();
226
+ ```
98
227
 
99
- ### Recommended UI behavior
228
+ ### Credentials submit handler
229
+
230
+ ```js
231
+ const handleLogin = async (body) => {
232
+ setApiError(undefined);
233
+
234
+ const res = await adminLoginWithOtp({
235
+ ...body,
236
+ deviceId: crypto.randomUUID(),
237
+ });
238
+
239
+ if ('error' in res) {
240
+ setApiError(res.error.message ?? 'Something went wrong');
241
+ return;
242
+ }
100
243
 
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
244
+ setOtpStep({
245
+ challengeId: res.data.challengeId,
246
+ expiresAt: res.data.expiresAt,
247
+ maskedEmail: res.data.maskedEmail,
248
+ rememberMe: body.rememberMe,
249
+ });
250
+ };
251
+ ```
252
+
253
+ ### OTP verify handler
254
+
255
+ ```js
256
+ const handleVerifyOtp = async ({ code }) => {
257
+ if (!otpStep) return;
258
+
259
+ setApiError(undefined);
260
+
261
+ const res = await verifyAdminLoginOtp({
262
+ challengeId: otpStep.challengeId,
263
+ code,
264
+ });
265
+
266
+ if ('error' in res) {
267
+ setApiError(res.error.message ?? 'Something went wrong');
268
+ return;
269
+ }
270
+
271
+ dispatch(
272
+ login({
273
+ token: res.data.token,
274
+ persist: otpStep.rememberMe,
275
+ })
276
+ );
277
+
278
+ navigate('/');
279
+ };
280
+ ```
281
+
282
+ ### OTP resend handler
283
+
284
+ ```js
285
+ const handleResendOtp = async () => {
286
+ if (!otpStep) return;
287
+
288
+ setApiError(undefined);
289
+
290
+ const res = await resendAdminLoginOtp({
291
+ challengeId: otpStep.challengeId,
292
+ });
293
+
294
+ if ('error' in res) {
295
+ setApiError(res.error.message ?? 'Something went wrong');
296
+ return;
297
+ }
298
+
299
+ setOtpStep((current) =>
300
+ current
301
+ ? {
302
+ ...current,
303
+ expiresAt: res.data.expiresAt,
304
+ maskedEmail: res.data.maskedEmail,
305
+ }
306
+ : current
307
+ );
308
+ };
309
+ ```
105
310
 
106
- ## Integration Guide
311
+ ### OTP input handling
107
312
 
108
- ### Login request
313
+ At minimum:
314
+
315
+ ```js
316
+ const OTP_LENGTH = 6;
317
+ const sanitizeOtp = (value = '') => value.replace(/\D/g, '').slice(0, OTP_LENGTH);
318
+ ```
319
+
320
+ The working implementation used in the companion project includes:
321
+
322
+ - 6 digit boxes
323
+ - paste support
324
+ - backspace handling
325
+ - auto focus
326
+ - inline error state
327
+
328
+ ## Step 7: Add A Patch Apply Script
329
+
330
+ Create:
331
+
332
+ - `scripts/apply-strapi-admin-2fa-patch.js`
333
+
334
+ This script should:
335
+
336
+ 1. copy `scripts/strapi-admin-2fa-patch/services/auth.js`
337
+ 2. copy `scripts/strapi-admin-2fa-patch/services/auth.mjs`
338
+ 3. copy `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.js`
339
+ 4. copy `scripts/strapi-admin-2fa-patch/pages/Auth/components/Login.mjs`
340
+ 5. overwrite the matching files in `node_modules/@strapi/admin/...`
341
+ 6. clear stale Strapi admin cache directories
342
+
343
+ ## Step 8: Wire The Patch Script In `package.json`
344
+
345
+ In your Strapi project `package.json`, add:
346
+
347
+ ```json
348
+ {
349
+ "scripts": {
350
+ "prebuild": "node scripts/apply-strapi-admin-2fa-patch.js",
351
+ "predev": "node scripts/apply-strapi-admin-2fa-patch.js",
352
+ "predevelop": "node scripts/apply-strapi-admin-2fa-patch.js",
353
+ "postinstall": "node scripts/apply-strapi-admin-2fa-patch.js"
354
+ }
355
+ }
356
+ ```
357
+
358
+ This ensures the login patch is reapplied:
359
+
360
+ - after dependency install
361
+ - before build
362
+ - before dev
363
+
364
+ ## Request Flow
365
+
366
+ ### Login
109
367
 
110
368
  ```http
111
369
  POST /api/admin-2fa/login
112
370
  Content-Type: application/json
113
371
  ```
114
372
 
115
- Example payload:
373
+ Example body:
116
374
 
117
375
  ```json
118
376
  {
@@ -123,12 +381,12 @@ Example payload:
123
381
  }
124
382
  ```
125
383
 
126
- Example success response:
384
+ Example response:
127
385
 
128
386
  ```json
129
387
  {
130
388
  "data": {
131
- "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
389
+ "challengeId": "***",
132
390
  "expiresAt": "2026-04-05T18:30:00.000Z",
133
391
  "maskedEmail": "admin@example.com",
134
392
  "rememberMe": true
@@ -136,89 +394,37 @@ Example success response:
136
394
  }
137
395
  ```
138
396
 
139
- ### Verify request
397
+ ### Verify
140
398
 
141
399
  ```http
142
400
  POST /api/admin-2fa/verify
143
401
  Content-Type: application/json
144
402
  ```
145
403
 
146
- Example payload:
404
+ Example body:
147
405
 
148
406
  ```json
149
407
  {
150
- "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
408
+ "challengeId": "***",
151
409
  "code": "123456"
152
410
  }
153
411
  ```
154
412
 
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
- ```
169
-
170
- ### Resend request
413
+ ### Resend
171
414
 
172
415
  ```http
173
416
  POST /api/admin-2fa/resend
174
417
  Content-Type: application/json
175
418
  ```
176
419
 
177
- Example payload:
420
+ Example body:
178
421
 
179
422
  ```json
180
423
  {
181
- "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4"
424
+ "challengeId": "***"
182
425
  }
183
426
  ```
184
427
 
185
- ### UI error states to handle
186
-
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
193
-
194
- ## Host Project Requirements
195
-
196
- ### Email provider
197
-
198
- The plugin sends OTP emails through Strapi's email plugin, so the host project must configure an email provider.
199
-
200
- ### Proxy and HTTPS
201
-
202
- If the project runs behind a reverse proxy, configure `config/server.ts` correctly so secure admin cookies work.
203
-
204
- Typical example:
205
-
206
- ```ts
207
- import type { Core } from "@strapi/strapi";
208
-
209
- const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
210
- host: env("HOST", "0.0.0.0"),
211
- port: env.int("PORT", 1337),
212
- url: env("URL", "http://localhost:1337"),
213
- proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
214
- app: {
215
- keys: env.array("APP_KEYS"),
216
- },
217
- });
218
-
219
- export default config;
220
- ```
221
-
222
428
  ## Environment Variables
223
429
 
224
430
  Suggested defaults:
@@ -238,68 +444,19 @@ ADMIN_OTP_RESEND_EMAIL_LIMIT=5
238
444
  ADMIN_OTP_DEBUG_TIMINGS=false
239
445
  ```
240
446
 
241
- ## Code-Level Architecture
242
-
243
- Main files:
244
-
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
252
- ```
253
-
254
- Responsibilities:
255
-
256
- - `admin/src/index.js`
257
- Minimal admin plugin stub required by the Strapi Plugin SDK.
258
-
259
- - `server/src/routes/index.js`
260
- Declares the login, verify, and resend routes.
261
-
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.
264
-
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.
267
-
268
- - `server/src/utils/strapi-session-auth.js`
269
- Runtime helper that resolves Strapi's internal admin session utility for final session creation.
270
-
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
279
-
280
- ```bash
281
- npm install
282
- npm run build
283
- ```
284
-
285
- Useful commands:
286
-
287
- - `npm run build`
288
- - `npm run watch`
289
- - `npm run watch:link`
290
- - `npm run verify`
447
+ ## Testing Checklist
291
448
 
292
- ## Publishing Checklist
449
+ After setup, test these cases:
293
450
 
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`
451
+ 1. correct email/password shows OTP screen
452
+ 2. correct OTP logs into admin successfully
453
+ 3. resend OTP works
454
+ 4. invalid OTP shows an error
455
+ 5. expired OTP restarts the flow properly
456
+ 6. wrong email/password still fails safely
300
457
 
301
458
  ## Production Notes
302
459
 
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.
460
+ - This improves admin security, but email OTP is still weaker than TOTP or WebAuthn.
461
+ - If the admin email account is compromised, the second factor can still be bypassed.
462
+ - For stronger future versions, consider TOTP, backup codes, trusted devices, or passkeys.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vivinkv28/strapi-2fa-admin-plugin",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Reusable Strapi admin 2FA plugin",
5
5
  "type": "commonjs",
6
6
  "keywords": [