@adminforth/two-factors-auth 3.0.5 → 3.1.1

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 CHANGED
@@ -1,7 +1,31 @@
1
1
  # AdminForth TwoFactorsAuth Plugin
2
2
 
3
- <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /> <img src="https://woodpecker.devforth.io/api/badges/3848/status.svg" alt="Build Status" /> <a href="https://www.npmjs.com/package/@adminforth/two-factors-auth"> <img src="https://img.shields.io/npm/dt/@adminforth/two-factors-auth" alt="npm downloads" /></a> <a href="https://www.npmjs.com/package/@adminforth/two-factors-auth"><img src="https://img.shields.io/npm/v/@adminforth/two-factors-auth" alt="npm version" /></a> <a href="https://www.npmjs.com/package/@adminforth/two-factors-auth">
3
+ <img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT" /> <img src="https://woodpecker.devforth.io/api/badges/3848/status.svg" alt="Build Status" /> <a href="https://www.npmjs.com/package/@adminforth/two-factors-auth"><img src="https://img.shields.io/npm/dm/@adminforth/two-factors-auth" alt="npm downloads" /></a> <a href="https://www.npmjs.com/package/@adminforth/two-factors-auth"><img src="https://img.shields.io/npm/v/@adminforth/two-factors-auth" alt="npm version" /></a>
4
+
5
+ [![Ask AI](https://tluma.ai/badge)](https://tluma.ai/ask-ai/devforth/adminforth)
4
6
 
5
7
  Allows to enable Two-Factor Authentication to an adminforth application.
6
8
 
7
- ## For usage, see [AdminForth TwoFactorsAuth Documentation](https://adminforth.dev/docs/tutorial/Plugins/TwoFactorsAuth/)
9
+ ## Features
10
+
11
+ - Add two-factor authentication to AdminForth login.
12
+ - Improve account security for admin users.
13
+ - Introduce a stronger verification step at sign-in.
14
+ - Integrate 2FA into existing authentication workflows.
15
+
16
+ ## Documentation
17
+
18
+ Full setup and configuration guide:
19
+
20
+ [AdminForth TwoFactorsAuth Documentation](https://adminforth.dev/docs/tutorial/Plugins/two-factors-auth/)
21
+
22
+ ## About AdminForth
23
+
24
+ AdminForth is an open-source, agent-first admin framework for building robust admin panels and back-office applications faster.
25
+
26
+ ## Related links
27
+
28
+ - [AdminForth website](https://adminforth.dev)
29
+ - [npm package](https://www.npmjs.com/package/@adminforth/two-factors-auth)
30
+ - [More AdminForth plugins](https://adminforth.dev/docs/tutorial/ListOfPlugins/)
31
+ - [Built by DevForth](https://devforth.io)
package/build.log CHANGED
@@ -16,5 +16,5 @@ custom/pnpm-lock.yaml
16
16
  custom/tsconfig.json
17
17
  custom/utils.js
18
18
 
19
- sent 280,853 bytes received 229 bytes 562,164.00 bytes/sec
20
- total size is 279,947 speedup is 1.00
19
+ sent 282,653 bytes received 229 bytes 565,764.00 bytes/sec
20
+ total size is 281,759 speedup is 1.00
@@ -79,8 +79,9 @@
79
79
  import { Link, Button } from '@/afcl';
80
80
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
81
81
  import { getPasskey } from './utils.js'
82
- import adminforth from '@/adminforth';
83
-
82
+ import { useAdminforth } from '@/adminforth';
83
+ import websocket from '@/websocket';
84
+ import type { AdminUser } from '@/types/Common';
84
85
 
85
86
  declare global {
86
87
  interface Window {
@@ -94,7 +95,51 @@
94
95
  }
95
96
  const props = defineProps<{
96
97
  autoFinishLogin?: boolean
98
+ adminUser?: AdminUser
97
99
  }>();
100
+
101
+ const { alert } = useAdminforth();
102
+
103
+ let currentSessionId: string | null = null;
104
+ watch( props, () => {
105
+ if (props.adminUser) {
106
+ websocket.unsubscribeByPrefix(`/user2fa/`);
107
+ websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
108
+ currentSessionId = data.sessionId;
109
+ let confirmationResult;
110
+ try {
111
+ confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
112
+ } catch (error) {
113
+ console.error('Error during 2FA confirmation:', error);
114
+ }
115
+ try {
116
+ const response = await callAdminForthApi({
117
+ method: "POST",
118
+ path: "/plugin/passkeys/resolveVerifyAuto",
119
+ body: { confirmationResult, sessionId: data.sessionId }
120
+ });
121
+ if (!response.ok && response.error === 'No session ID or confirmation result'){
122
+ alert({message: 'Verification session finished or cancelled.', variant: 'warning'});
123
+ } else if (!response.ok) {
124
+ alert({message: 'Verification failed', variant: 'danger'});
125
+ } else if (response.ok) {
126
+ alert({message: 'Verification successful', variant: 'success'});
127
+ }
128
+ } catch (error) {
129
+ console.error('Error resolving automatic 2FA verification:', error);
130
+ }
131
+ currentSessionId = null;
132
+ });
133
+ websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
134
+ if (currentSessionId === data.sessionId && rejectFn && modelShow.value) {
135
+ onCancel();
136
+ currentSessionId = null;
137
+ }
138
+ });
139
+ }
140
+ })
141
+
142
+
98
143
  const emit = defineEmits<{
99
144
  (e: 'resolved', payload: any): void
100
145
  (e: 'rejected', err?: any): void
@@ -6,7 +6,7 @@
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
7
7
  },
8
8
  "keywords": [],
9
- "author": "",
9
+ "author": "DevForth (https://devforth.io)",
10
10
  "license": "ISC",
11
11
  "description": "",
12
12
  "dependencies": {
@@ -79,8 +79,9 @@
79
79
  import { Link, Button } from '@/afcl';
80
80
  import { IconShieldOutline } from '@iconify-prerendered/vue-flowbite';
81
81
  import { getPasskey } from './utils.js'
82
- import adminforth from '@/adminforth';
83
-
82
+ import { useAdminforth } from '@/adminforth';
83
+ import websocket from '@/websocket';
84
+ import type { AdminUser } from '@/types/Common';
84
85
 
85
86
  declare global {
86
87
  interface Window {
@@ -94,7 +95,51 @@
94
95
  }
95
96
  const props = defineProps<{
96
97
  autoFinishLogin?: boolean
98
+ adminUser?: AdminUser
97
99
  }>();
100
+
101
+ const { alert } = useAdminforth();
102
+
103
+ let currentSessionId: string | null = null;
104
+ watch( props, () => {
105
+ if (props.adminUser) {
106
+ websocket.unsubscribeByPrefix(`/user2fa/`);
107
+ websocket.subscribe(`/user2fa/${props.adminUser.pk}`, async (data: {sessionId: string}) => {
108
+ currentSessionId = data.sessionId;
109
+ let confirmationResult;
110
+ try {
111
+ confirmationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
112
+ } catch (error) {
113
+ console.error('Error during 2FA confirmation:', error);
114
+ }
115
+ try {
116
+ const response = await callAdminForthApi({
117
+ method: "POST",
118
+ path: "/plugin/passkeys/resolveVerifyAuto",
119
+ body: { confirmationResult, sessionId: data.sessionId }
120
+ });
121
+ if (!response.ok && response.error === 'No session ID or confirmation result'){
122
+ alert({message: 'Verification session finished or cancelled.', variant: 'warning'});
123
+ } else if (!response.ok) {
124
+ alert({message: 'Verification failed', variant: 'danger'});
125
+ } else if (response.ok) {
126
+ alert({message: 'Verification successful', variant: 'success'});
127
+ }
128
+ } catch (error) {
129
+ console.error('Error resolving automatic 2FA verification:', error);
130
+ }
131
+ currentSessionId = null;
132
+ });
133
+ websocket.subscribe(`/user2fa/${props.adminUser.pk}-resolve`, async (data: {sessionId: string}) => {
134
+ if (currentSessionId === data.sessionId && rejectFn && modelShow.value) {
135
+ onCancel();
136
+ currentSessionId = null;
137
+ }
138
+ });
139
+ }
140
+ })
141
+
142
+
98
143
  const emit = defineEmits<{
99
144
  (e: 'resolved', payload: any): void
100
145
  (e: 'rejected', err?: any): void
@@ -6,7 +6,7 @@
6
6
  "test": "echo \"Error: no test specified\" && exit 1"
7
7
  },
8
8
  "keywords": [],
9
- "author": "",
9
+ "author": "DevForth (https://devforth.io)",
10
10
  "license": "ISC",
11
11
  "description": "",
12
12
  "dependencies": {
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import crypto from 'crypto';
16
16
  export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
17
17
  constructor(options) {
18
18
  super(options, import.meta.url);
19
+ this.pending = new Map();
19
20
  this.options = options;
20
21
  this.shouldHaveSingleInstancePerWholeApp = () => true;
21
22
  }
@@ -133,17 +134,38 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
133
134
  return false;
134
135
  });
135
136
  }
137
+ waitForResponse(id) {
138
+ return new Promise((resolve) => {
139
+ this.pending.set(id, resolve);
140
+ });
141
+ }
142
+ resolveResponse(id, data) {
143
+ const resolve = this.pending.get(id);
144
+ if (resolve) {
145
+ resolve(data);
146
+ this.pending.delete(id);
147
+ }
148
+ }
149
+ verifyAuto(adminUser) {
150
+ return __awaiter(this, void 0, void 0, function* () {
151
+ const sessionId = crypto.randomUUID();
152
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId });
153
+ const result = yield this.waitForResponse(sessionId);
154
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId });
155
+ return result;
156
+ });
157
+ }
136
158
  verify(confirmationResult, opts) {
137
159
  return __awaiter(this, void 0, void 0, function* () {
138
- var _a, _b, _c, _d, _e;
160
+ var _a, _b, _c, _d, _e, _f, _g;
139
161
  if (!confirmationResult)
140
162
  return { error: "Confirmation result is required" };
141
163
  if (!opts.adminUser)
142
164
  return { error: "Admin user is required" };
143
165
  if (!opts.userPk)
144
166
  return { error: "User PK is required" };
145
- const cookies = opts.extra.cookies || opts.cookies;
146
- const response = opts.extra.response || opts.response;
167
+ const cookies = ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.cookies) || opts.cookies;
168
+ const response = ((_b = opts.extra) === null || _b === void 0 ? void 0 : _b.response) || opts.response;
147
169
  if (this.options.usersFilterToApply) {
148
170
  const res = this.options.usersFilterToApply(opts.adminUser);
149
171
  if (res === false) {
@@ -156,7 +178,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
156
178
  return { ok: true };
157
179
  }
158
180
  }
159
- if (this.options.stepUpMfaGracePeriodSeconds && ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.headers) && !confirmationResult.mode) {
181
+ if (this.options.stepUpMfaGracePeriodSeconds && ((_c = opts.extra) === null || _c === void 0 ? void 0 : _c.headers) && !confirmationResult.mode) {
160
182
  const verificationResult = yield this.isTempSkip2FAGraceValid(opts.extra.headers, cookies);
161
183
  if (verificationResult === true) {
162
184
  return { ok: true };
@@ -169,10 +191,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
169
191
  if (!authRes)
170
192
  return { error: "Auth resource not found" };
171
193
  const connector = this.adminforth.connectors[authRes.dataSource];
172
- const pkName = (_b = authRes.columns.find(c => c.primaryKey)) === null || _b === void 0 ? void 0 : _b.name;
194
+ const pkName = (_d = authRes.columns.find(c => c.primaryKey)) === null || _d === void 0 ? void 0 : _d.name;
173
195
  if (!pkName)
174
196
  return { error: "Primary key not found on auth resource" };
175
- const pk = (_c = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _c !== void 0 ? _c : (_e = (_d = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _d === void 0 ? void 0 : _d.dbUser) === null || _e === void 0 ? void 0 : _e[pkName];
197
+ const pk = (_e = opts === null || opts === void 0 ? void 0 : opts.userPk) !== null && _e !== void 0 ? _e : (_g = (_f = opts === null || opts === void 0 ? void 0 : opts.adminUser) === null || _f === void 0 ? void 0 : _f.dbUser) === null || _g === void 0 ? void 0 : _g[pkName];
176
198
  if (!pk)
177
199
  return { error: "User PK is required" };
178
200
  const user = yield connector.getRecordByPrimaryKey(authRes, pk);
@@ -695,7 +717,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
695
717
  method: 'POST',
696
718
  path: `/plugin/passkeys/registerPasskeyRequest`,
697
719
  noAuth: false,
698
- handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies, extra }) {
720
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
699
721
  var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
700
722
  const mode = body === null || body === void 0 ? void 0 : body.mode;
701
723
  const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
@@ -703,8 +725,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
703
725
  adminUser: adminUser,
704
726
  userPk: adminUser.pk,
705
727
  cookies: cookies,
706
- response: response,
707
- extra: Object.assign({}, extra)
728
+ response: response
708
729
  });
709
730
  if (!verificationResult || !('ok' in verificationResult)) {
710
731
  return { ok: false, error: 'Verification failed' };
@@ -990,5 +1011,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
990
1011
  }
991
1012
  })
992
1013
  });
1014
+ server.endpoint({
1015
+ method: 'POST',
1016
+ path: `/plugin/passkeys/resolveVerifyAuto`,
1017
+ noAuth: false,
1018
+ handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
1019
+ const sessionId = body === null || body === void 0 ? void 0 : body.sessionId;
1020
+ const confirmationResult = body === null || body === void 0 ? void 0 : body.confirmationResult;
1021
+ if (!sessionId || !confirmationResult) {
1022
+ this.resolveResponse(sessionId, { ok: false, error: 'No session ID or confirmation result' });
1023
+ return { ok: false, error: 'No session ID or confirmation result' };
1024
+ }
1025
+ const verificationResult = yield this.verify(confirmationResult, {
1026
+ adminUser: adminUser,
1027
+ userPk: adminUser.pk,
1028
+ cookies: cookies,
1029
+ response: response
1030
+ });
1031
+ if (!verificationResult || !('ok' in verificationResult)) {
1032
+ this.resolveResponse(sessionId, { ok: false, error: 'Verification failed' });
1033
+ return { ok: false, error: 'Verification failed' };
1034
+ }
1035
+ this.resolveResponse(sessionId, { ok: true, passkeyConfirmed: verificationResult });
1036
+ return { ok: true };
1037
+ })
1038
+ });
993
1039
  }
994
1040
  }
package/index.ts CHANGED
@@ -141,6 +141,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
141
141
  return false;
142
142
  }
143
143
 
144
+ private pending = new Map<string, (value: any) => void>();
145
+
146
+ private waitForResponse(id: string): Promise<any> {
147
+ return new Promise((resolve) => {
148
+ this.pending.set(id, resolve);
149
+ });
150
+ }
151
+
152
+ private resolveResponse(id: string, data: any) {
153
+ const resolve = this.pending.get(id);
154
+ if (resolve) {
155
+ resolve(data);
156
+ this.pending.delete(id);
157
+ }
158
+ }
159
+
160
+ public async verifyAuto(adminUser: AdminUser) {
161
+ const sessionId = crypto.randomUUID();
162
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}`, { sessionId });
163
+ const result = await this.waitForResponse(sessionId);
164
+ this.adminforth.websocket.publish(`/user2fa/${adminUser.pk}-resolve`, { sessionId });
165
+ return result;
166
+ }
167
+
144
168
  public async verify(
145
169
  confirmationResult: Record<string, any>,
146
170
  opts?: { adminUser?: AdminUser; userPk?: string; cookies?: any, response?: IAdminForthHttpResponse, extra?: HttpExtra }
@@ -148,8 +172,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
148
172
  if (!confirmationResult) return { error: "Confirmation result is required" };
149
173
  if (!opts.adminUser) return { error: "Admin user is required" };
150
174
  if (!opts.userPk) return { error: "User PK is required" };
151
- const cookies = opts.extra.cookies || opts.cookies;
152
- const response = opts.extra.response || opts.response;
175
+ const cookies = opts.extra?.cookies || opts.cookies;
176
+ const response = opts.extra?.response || opts.response;
153
177
  if (this.options.usersFilterToApply) {
154
178
  const res = this.options.usersFilterToApply(opts.adminUser);
155
179
  if ( res === false ) {
@@ -753,7 +777,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
753
777
  method: 'POST',
754
778
  path: `/plugin/passkeys/registerPasskeyRequest`,
755
779
  noAuth: false,
756
- handler: async ({ body, adminUser, response, cookies, extra }) => {
780
+ handler: async ({ body, adminUser, response, cookies }) => {
757
781
  const mode = body?.mode;
758
782
 
759
783
  const confirmationResult = body?.confirmationResult;
@@ -761,8 +785,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
761
785
  adminUser: adminUser,
762
786
  userPk: adminUser.pk,
763
787
  cookies: cookies,
764
- response: response,
765
- extra: { ...extra }
788
+ response: response
766
789
  });
767
790
  if ( !verificationResult || !('ok' in verificationResult) ) {
768
791
  return { ok: false, error: 'Verification failed' };
@@ -1049,5 +1072,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
1049
1072
  }
1050
1073
  }
1051
1074
  });
1075
+ server.endpoint({
1076
+ method: 'POST',
1077
+ path: `/plugin/passkeys/resolveVerifyAuto`,
1078
+ noAuth: false,
1079
+ handler: async ({ body, adminUser, response, cookies }) => {
1080
+ const sessionId = body?.sessionId;
1081
+ const confirmationResult = body?.confirmationResult;
1082
+ if (!sessionId || !confirmationResult) {
1083
+ this.resolveResponse(sessionId, { ok: false, error: 'No session ID or confirmation result' });
1084
+ return { ok: false, error: 'No session ID or confirmation result' };
1085
+ }
1086
+ const verificationResult = await this.verify(confirmationResult, {
1087
+ adminUser: adminUser,
1088
+ userPk: adminUser.pk,
1089
+ cookies: cookies,
1090
+ response: response
1091
+ });
1092
+ if ( !verificationResult || !('ok' in verificationResult) ) {
1093
+ this.resolveResponse(sessionId, { ok: false, error: 'Verification failed' });
1094
+ return { ok: false, error: 'Verification failed' };
1095
+ }
1096
+ this.resolveResponse(sessionId, { ok: true, passkeyConfirmed: verificationResult });
1097
+ return { ok: true };
1098
+ }
1099
+ });
1052
1100
  }
1053
1101
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@adminforth/two-factors-auth",
3
- "version": "3.0.5",
3
+ "version": "3.1.1",
4
4
  "description": "AdminForth plugin for two factors authentication",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
- "homepage": "https://adminforth.dev/docs/tutorial/Plugins/TwoFactorsAuth/",
10
+ "homepage": "https://adminforth.dev/docs/tutorial/Plugins/two-factors-auth/",
11
11
  "repository": {
12
12
  "type": "git",
13
13
  "url": "https://github.com/devforth/adminforth-two-factors-auth.git"
@@ -21,14 +21,14 @@
21
21
  "two-factors-auth",
22
22
  "2fa"
23
23
  ],
24
- "author": "devforth",
24
+ "author": "DevForth (https://devforth.io)",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
27
  "@simplewebauthn/server": "^13.2.1",
28
28
  "node-2fa": "^2.0.3"
29
29
  },
30
30
  "peerDependencies": {
31
- "adminforth": "^2.30.0"
31
+ "adminforth": "^2.42.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.10.7",