@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.
@@ -0,0 +1,203 @@
1
+ # Architecture Guide
2
+
3
+ This guide explains the internal structure of `@vivinkv28/strapi-2fa-admin-plugin` for maintainers and contributors.
4
+
5
+ ## High-Level Design
6
+
7
+ The plugin is a backend-focused Strapi 5 plugin with a minimal admin stub.
8
+
9
+ Its responsibilities are:
10
+
11
+ - validate Strapi admin credentials
12
+ - create a temporary OTP challenge
13
+ - send an OTP by email
14
+ - verify the OTP
15
+ - create the final Strapi admin session only after OTP verification
16
+
17
+ Its non-responsibilities are:
18
+
19
+ - replacing the Strapi admin login UI
20
+ - rendering a custom OTP page inside the admin by itself
21
+
22
+ ## Folder Structure
23
+
24
+ ```text
25
+ admin/src/index.js
26
+ server/src/index.js
27
+ server/src/routes/index.js
28
+ server/src/controllers/auth.js
29
+ server/src/services/auth.js
30
+ server/src/utils/strapi-session-auth.js
31
+ ```
32
+
33
+ ## Entry Points
34
+
35
+ ### `admin/src/index.js`
36
+
37
+ This is a minimal admin-side plugin entry required by the Strapi Plugin SDK.
38
+
39
+ Right now it only provides:
40
+
41
+ - `register()`
42
+ - `bootstrap()`
43
+ - `registerTrads()`
44
+
45
+ It does not add real admin UI behavior.
46
+
47
+ ### `server/src/index.js`
48
+
49
+ This is the server-side plugin entry. It wires together:
50
+
51
+ - controllers
52
+ - routes
53
+ - services
54
+
55
+ ## Routes
56
+
57
+ Defined in [`server/src/routes/index.js`](../server/src/routes/index.js).
58
+
59
+ The plugin registers a `content-api` router with these logical paths:
60
+
61
+ - `/login`
62
+ - `/verify`
63
+ - `/resend`
64
+
65
+ Because Strapi prefixes plugin content-api routes with the plugin name, these become:
66
+
67
+ - `/api/admin-2fa/login`
68
+ - `/api/admin-2fa/verify`
69
+ - `/api/admin-2fa/resend`
70
+
71
+ ## Controller Layer
72
+
73
+ Defined in [`server/src/controllers/auth.js`](../server/src/controllers/auth.js).
74
+
75
+ The controller is intentionally thin:
76
+
77
+ - reads request body
78
+ - reads client IP from proxy-aware request headers
79
+ - calls the auth service
80
+ - sets the Strapi admin refresh cookie after successful OTP verification
81
+ - returns response data in Strapi-style `{ data: ... }` format
82
+
83
+ ### Client IP handling
84
+
85
+ The controller checks:
86
+
87
+ 1. `x-forwarded-for`
88
+ 2. `ctx.request.ip`
89
+ 3. `ctx.ip`
90
+
91
+ This is important because the service rate-limits by IP.
92
+
93
+ ## Service Layer
94
+
95
+ Defined in [`server/src/services/auth.js`](../server/src/services/auth.js).
96
+
97
+ This is the core of the plugin.
98
+
99
+ ### Main responsibilities
100
+
101
+ - read plugin config from `strapi.config.get("plugin::admin-2fa")`
102
+ - normalize and validate request input
103
+ - call `strapi.service("admin::auth").checkCredentials(...)`
104
+ - generate OTP codes
105
+ - hash OTP values with `crypto.scrypt`
106
+ - store challenge state in `strapi.store`
107
+ - enforce resend/verify/login rate limits
108
+ - send OTP email using Strapi email plugin
109
+ - create the final Strapi admin session after OTP verification
110
+
111
+ ### Important internal helpers
112
+
113
+ #### `getPluginConfig()`
114
+
115
+ Builds the runtime configuration using plugin config values with defaults.
116
+
117
+ #### `createOtpCode()`
118
+
119
+ Generates a numeric OTP code with configurable length.
120
+
121
+ #### `createOtpHash()`
122
+
123
+ Hashes the OTP using:
124
+
125
+ - `challengeId`
126
+ - `code`
127
+ - random `salt`
128
+
129
+ The raw OTP is never stored.
130
+
131
+ #### `registerRateLimitHit()`
132
+
133
+ Stores rate-limit counters in `strapi.store`.
134
+
135
+ Rate-limit keys are hashed with SHA-256 so raw identifiers are not used directly as store keys.
136
+
137
+ #### `createSession()`
138
+
139
+ Uses Strapi's internal admin session helpers to generate:
140
+
141
+ - refresh token
142
+ - cookie options
143
+ - access token
144
+ - sanitized admin user
145
+
146
+ ## Challenge Storage
147
+
148
+ The plugin uses `strapi.store()` with:
149
+
150
+ - type: `plugin`
151
+ - name: `admin-otp-login`
152
+
153
+ This is used for:
154
+
155
+ - OTP challenges
156
+ - rate-limit entries
157
+
158
+ This keeps OTP state internal to Strapi rather than creating a public collection type.
159
+
160
+ ## Session Helper
161
+
162
+ Defined in [`server/src/utils/strapi-session-auth.js`](../server/src/utils/strapi-session-auth.js).
163
+
164
+ Strapi does not expose the needed admin session helper directly as a simple public import for this use case, so the plugin resolves the installed Strapi admin `session-auth.js` helper at runtime.
165
+
166
+ This is one of the more sensitive parts of the implementation because it depends on Strapi internal package layout.
167
+
168
+ ## Security Model
169
+
170
+ ### Good properties
171
+
172
+ - password alone does not create a final admin session
173
+ - OTP is hashed, not stored in plain text
174
+ - OTPs expire
175
+ - resend attempts are limited
176
+ - verify attempts are limited
177
+ - login/verify/resend rate limits exist
178
+
179
+ ### Limitations
180
+
181
+ - this is email OTP, not TOTP or WebAuthn
182
+ - if the admin email account is compromised, the second factor can still be bypassed
183
+ - session creation still depends on Strapi internal admin session utilities
184
+
185
+ ## Extension Points
186
+
187
+ If you want to extend this plugin later, common directions are:
188
+
189
+ - customizable email templates
190
+ - audit logging
191
+ - backup codes
192
+ - trusted devices
193
+ - TOTP support
194
+ - WebAuthn/passkeys
195
+
196
+ ## Integration Boundary
197
+
198
+ The clean boundary is:
199
+
200
+ - plugin: backend 2FA engine
201
+ - host app: admin login UI interception and OTP screen behavior
202
+
203
+ That split is intentional and should stay explicit in docs and versioning.
@@ -0,0 +1,246 @@
1
+ # Integration Guide
2
+
3
+ This guide explains how to use `@vivinkv28/strapi-2fa-admin-plugin` inside an existing Strapi 5 project.
4
+
5
+ ## What The Plugin Does
6
+
7
+ The plugin provides the backend endpoints and auth logic for an OTP-based admin login flow.
8
+
9
+ It does not replace the Strapi admin login UI by itself.
10
+
11
+ Your host project must provide a login integration layer that:
12
+
13
+ 1. collects admin email and password
14
+ 2. calls the plugin login endpoint
15
+ 3. shows an OTP input screen
16
+ 4. calls the plugin verify endpoint
17
+ 5. optionally allows OTP resend
18
+
19
+ ## Endpoints
20
+
21
+ The plugin registers these routes:
22
+
23
+ - `POST /api/admin-2fa/login`
24
+ - `POST /api/admin-2fa/verify`
25
+ - `POST /api/admin-2fa/resend`
26
+
27
+ ## Install And Enable
28
+
29
+ ### Published npm package
30
+
31
+ ```bash
32
+ npm install @vivinkv28/strapi-2fa-admin-plugin
33
+ ```
34
+
35
+ ### Local external plugin
36
+
37
+ In your Strapi app:
38
+
39
+ ```ts
40
+ // config/plugins.ts
41
+ import type { Core } from "@strapi/strapi";
42
+
43
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
44
+ "admin-2fa": {
45
+ enabled: true,
46
+ resolve: "../strapi-2fa-admin-plugin",
47
+ config: {
48
+ otpDigits: env.int("ADMIN_OTP_DIGITS", 6),
49
+ otpTtlSeconds: env.int("ADMIN_OTP_TTL_SECONDS", 300),
50
+ maxAttempts: env.int("ADMIN_OTP_MAX_ATTEMPTS", 5),
51
+ maxResends: env.int("ADMIN_OTP_MAX_RESENDS", 3),
52
+ rateLimitWindowSeconds: env.int("ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS", 900),
53
+ loginIpLimit: env.int("ADMIN_OTP_LOGIN_IP_LIMIT", 10),
54
+ loginEmailLimit: env.int("ADMIN_OTP_LOGIN_EMAIL_LIMIT", 5),
55
+ verifyIpLimit: env.int("ADMIN_OTP_VERIFY_IP_LIMIT", 20),
56
+ verifyEmailLimit: env.int("ADMIN_OTP_VERIFY_EMAIL_LIMIT", 10),
57
+ resendIpLimit: env.int("ADMIN_OTP_RESEND_IP_LIMIT", 10),
58
+ resendEmailLimit: env.int("ADMIN_OTP_RESEND_EMAIL_LIMIT", 5),
59
+ debugTimings: env.bool(
60
+ "ADMIN_OTP_DEBUG_TIMINGS",
61
+ env("NODE_ENV", "development") !== "production"
62
+ ),
63
+ },
64
+ },
65
+ });
66
+
67
+ export default config;
68
+ ```
69
+
70
+ ## Required Host App Setup
71
+
72
+ ### Email provider
73
+
74
+ The plugin calls `strapi.plugin("email").service("email").send(...)`.
75
+
76
+ Your Strapi project must configure an email provider or OTP delivery will fail.
77
+
78
+ ### Proxy and HTTPS
79
+
80
+ If your project runs behind a reverse proxy, configure `config/server.ts` correctly so Strapi recognizes secure requests and admin cookies are set with the right options.
81
+
82
+ Typical example:
83
+
84
+ ```ts
85
+ import type { Core } from "@strapi/strapi";
86
+
87
+ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
88
+ host: env("HOST", "0.0.0.0"),
89
+ port: env.int("PORT", 1337),
90
+ url: env("URL", "http://localhost:1337"),
91
+ proxy: env.bool("IS_PROXIED", env("NODE_ENV", "development") === "production"),
92
+ app: {
93
+ keys: env.array("APP_KEYS"),
94
+ },
95
+ });
96
+
97
+ export default config;
98
+ ```
99
+
100
+ ## Request Flow
101
+
102
+ ### 1. Start login
103
+
104
+ Send admin email and password to:
105
+
106
+ ```http
107
+ POST /api/admin-2fa/login
108
+ Content-Type: application/json
109
+ ```
110
+
111
+ Example payload:
112
+
113
+ ```json
114
+ {
115
+ "email": "admin@example.com",
116
+ "password": "super-secret-password",
117
+ "rememberMe": true,
118
+ "deviceId": "browser-device-id"
119
+ }
120
+ ```
121
+
122
+ Example success response:
123
+
124
+ ```json
125
+ {
126
+ "data": {
127
+ "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
128
+ "expiresAt": "2026-04-05T18:30:00.000Z",
129
+ "maskedEmail": "admin@example.com",
130
+ "rememberMe": true
131
+ }
132
+ }
133
+ ```
134
+
135
+ What happens here:
136
+
137
+ - Strapi validates the admin credentials
138
+ - the plugin creates an OTP challenge
139
+ - the plugin emails the OTP
140
+ - no final admin session is created yet
141
+
142
+ ### 2. Verify OTP
143
+
144
+ Send the OTP code to:
145
+
146
+ ```http
147
+ POST /api/admin-2fa/verify
148
+ Content-Type: application/json
149
+ ```
150
+
151
+ Example payload:
152
+
153
+ ```json
154
+ {
155
+ "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4",
156
+ "code": "123456"
157
+ }
158
+ ```
159
+
160
+ Example success response:
161
+
162
+ ```json
163
+ {
164
+ "data": {
165
+ "token": "<access-token>",
166
+ "accessToken": "<access-token>",
167
+ "user": {
168
+ "id": 1,
169
+ "email": "admin@example.com"
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ What happens here:
176
+
177
+ - the plugin reloads the OTP challenge
178
+ - the submitted code is hashed and compared
179
+ - the real Strapi admin session is created only after OTP verification
180
+ - the refresh cookie is set by the plugin controller
181
+
182
+ ### 3. Resend OTP
183
+
184
+ Send:
185
+
186
+ ```http
187
+ POST /api/admin-2fa/resend
188
+ Content-Type: application/json
189
+ ```
190
+
191
+ Example payload:
192
+
193
+ ```json
194
+ {
195
+ "challengeId": "0d3af6fd-b351-4d1e-bb81-2a8201d8a0f4"
196
+ }
197
+ ```
198
+
199
+ This generates a new OTP for the same challenge and extends the expiry.
200
+
201
+ ## Error Cases To Handle In The UI
202
+
203
+ Your login integration should handle these cases cleanly:
204
+
205
+ - invalid email or password
206
+ - OTP session not found
207
+ - OTP expired
208
+ - invalid OTP code
209
+ - too many authentication attempts
210
+ - maximum resend attempts exceeded
211
+
212
+ These are returned as normal Strapi error responses, so the frontend should surface the message to the admin user in a safe and friendly way.
213
+
214
+ ## Recommended Frontend Behavior
215
+
216
+ - Keep login and OTP entry as two separate UI states.
217
+ - Store `challengeId` in memory for the current login attempt.
218
+ - Do not create your own session after password validation; wait for `/verify`.
219
+ - After successful OTP verification, treat the returned user/token exactly like a successful admin login.
220
+ - If resend fails due to limits or expiry, restart the login flow.
221
+
222
+ ## Environment Variables
223
+
224
+ Recommended defaults:
225
+
226
+ ```env
227
+ ADMIN_OTP_DIGITS=6
228
+ ADMIN_OTP_TTL_SECONDS=300
229
+ ADMIN_OTP_MAX_ATTEMPTS=5
230
+ ADMIN_OTP_MAX_RESENDS=3
231
+ ADMIN_OTP_RATE_LIMIT_WINDOW_SECONDS=900
232
+ ADMIN_OTP_LOGIN_IP_LIMIT=10
233
+ ADMIN_OTP_LOGIN_EMAIL_LIMIT=5
234
+ ADMIN_OTP_VERIFY_IP_LIMIT=20
235
+ ADMIN_OTP_VERIFY_EMAIL_LIMIT=10
236
+ ADMIN_OTP_RESEND_IP_LIMIT=10
237
+ ADMIN_OTP_RESEND_EMAIL_LIMIT=5
238
+ ADMIN_OTP_DEBUG_TIMINGS=false
239
+ ```
240
+
241
+ ## Production Notes
242
+
243
+ - Email OTP is better than password-only login, but weaker than TOTP or WebAuthn.
244
+ - If the admin mailbox is compromised, the second factor can still be bypassed.
245
+ - Use app passwords or provider-managed SMTP credentials for OTP delivery.
246
+ - Make sure your host project sets `URL` and proxy settings correctly in production.
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@vivinkv28/strapi-2fa-admin-plugin",
3
+ "version": "0.1.0",
4
+ "description": "Reusable Strapi admin 2FA plugin",
5
+ "type": "commonjs",
6
+ "keywords": [
7
+ "strapi",
8
+ "strapi-plugin",
9
+ "strapi-v5",
10
+ "2fa",
11
+ "otp",
12
+ "admin-auth"
13
+ ],
14
+ "files": [
15
+ "dist",
16
+ "docs"
17
+ ],
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ "./strapi-admin": {
21
+ "source": "./admin/src/index.js",
22
+ "import": "./dist/admin/index.mjs",
23
+ "require": "./dist/admin/index.js",
24
+ "default": "./dist/admin/index.js"
25
+ },
26
+ "./strapi-server": {
27
+ "source": "./server/src/index.js",
28
+ "import": "./dist/server/index.mjs",
29
+ "require": "./dist/server/index.js",
30
+ "default": "./dist/server/index.js"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "build": "strapi-plugin build",
35
+ "watch": "strapi-plugin watch",
36
+ "watch:link": "strapi-plugin watch:link",
37
+ "verify": "strapi-plugin verify"
38
+ },
39
+ "engines": {
40
+ "node": ">=20.0.0 <=22.x.x",
41
+ "npm": ">=9.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "@strapi/sdk-plugin": "^5.0.0",
45
+ "prettier": "^3.0.0"
46
+ },
47
+ "peerDependencies": {
48
+ "@strapi/strapi": "^5.0.0",
49
+ "react": "^18.0.0",
50
+ "react-dom": "^18.0.0",
51
+ "react-router-dom": "^6.0.0",
52
+ "styled-components": "^6.0.0"
53
+ },
54
+ "strapi": {
55
+ "name": "admin-2fa",
56
+ "displayName": "Admin 2FA",
57
+ "description": "Adds OTP-based 2FA for Strapi admin authentication",
58
+ "kind": "plugin"
59
+ },
60
+ "license": "MIT"
61
+ }