@adminforth/two-factors-auth 1.9.2 → 1.10.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/build.log +3 -2
- package/custom/TwoFactorsConfirmation.vue +241 -55
- package/custom/TwoFactorsPasskeysSettings.vue +334 -0
- package/custom/TwoFactorsSetup.vue +15 -5
- package/custom/package-lock.json +7 -0
- package/custom/package.json +1 -0
- package/dist/custom/TwoFactorsConfirmation.vue +241 -55
- package/dist/custom/TwoFactorsPasskeysSettings.vue +334 -0
- package/dist/custom/TwoFactorsSetup.vue +15 -5
- package/dist/custom/package-lock.json +7 -0
- package/dist/custom/package.json +1 -0
- package/dist/index.js +449 -35
- package/index.ts +463 -37
- package/package.json +3 -1
- package/types.ts +62 -1
package/dist/index.js
CHANGED
|
@@ -7,8 +7,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
7
7
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
|
-
import { AdminForthPlugin } from "adminforth";
|
|
10
|
+
import { AdminForthPlugin, Filters } from "adminforth";
|
|
11
11
|
import twofactor from 'node-2fa';
|
|
12
|
+
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
13
|
+
import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
12
14
|
export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
13
15
|
constructor(options) {
|
|
14
16
|
super(options, import.meta.url);
|
|
@@ -43,22 +45,157 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
43
45
|
return verified ? { ok: true } : { error: "Wrong or expired OTP code" };
|
|
44
46
|
});
|
|
45
47
|
}
|
|
48
|
+
parsePeriod(period) {
|
|
49
|
+
if (!period)
|
|
50
|
+
return 0;
|
|
51
|
+
const match = /^(\d+)([dhms])$/.exec(period.trim());
|
|
52
|
+
if (!match)
|
|
53
|
+
throw new Error(`Invalid suggestionPeriod format: ${period}`);
|
|
54
|
+
const value = parseInt(match[1], 10);
|
|
55
|
+
const unit = match[2];
|
|
56
|
+
switch (unit) {
|
|
57
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
58
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
59
|
+
case 'm': return value * 60 * 1000;
|
|
60
|
+
case 's': return value * 1000;
|
|
61
|
+
default: return value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
verifyPasskeyResponse(body, user_id, cookies) {
|
|
65
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
66
|
+
var _a, _b, _c, _d, _e;
|
|
67
|
+
const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
|
|
68
|
+
const expectedOrigin = body.origin;
|
|
69
|
+
const expectedChallenge = cookies.challenge;
|
|
70
|
+
const expectedRPID = ((_d = (_c = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings) === null || _c === void 0 ? void 0 : _c.rp) === null || _d === void 0 ? void 0 : _d.id) || (new URL(settingsOrigin)).hostname;
|
|
71
|
+
const response = JSON.parse(body.response);
|
|
72
|
+
try {
|
|
73
|
+
if (settingsOrigin !== expectedOrigin) {
|
|
74
|
+
throw new Error('Invalid origin');
|
|
75
|
+
}
|
|
76
|
+
const cred = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, response.id)]);
|
|
77
|
+
if (!cred) {
|
|
78
|
+
throw new Error('Credential not found.');
|
|
79
|
+
}
|
|
80
|
+
const credMeta = JSON.parse(cred[this.options.passkeys.credentialMetaFieldName]);
|
|
81
|
+
if (!credMeta || !credMeta.public_key) {
|
|
82
|
+
throw new Error('Credential public key not found.');
|
|
83
|
+
}
|
|
84
|
+
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
85
|
+
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
86
|
+
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
87
|
+
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
88
|
+
const user = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, cred[this.options.passkeys.credentialUserIdFieldName])]);
|
|
89
|
+
if (!user || !user_id || user[usersPrimaryKeyFieldName] !== user_id) {
|
|
90
|
+
throw new Error('User not found.');
|
|
91
|
+
}
|
|
92
|
+
const { verified, authenticationInfo } = yield verifyAuthenticationResponse({
|
|
93
|
+
response,
|
|
94
|
+
expectedChallenge,
|
|
95
|
+
expectedOrigin,
|
|
96
|
+
expectedRPID,
|
|
97
|
+
credential: {
|
|
98
|
+
id: cred[this.options.passkeys.credentialIdFieldName],
|
|
99
|
+
publicKey: isoBase64URL.toBuffer(credMeta.public_key),
|
|
100
|
+
counter: credMeta.counter,
|
|
101
|
+
transports: credMeta.transports,
|
|
102
|
+
},
|
|
103
|
+
requireUserVerification: ((_e = this.options.passkeys) === null || _e === void 0 ? void 0 : _e.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
104
|
+
});
|
|
105
|
+
if (!verified) {
|
|
106
|
+
return { ok: false, error: 'User verification failed.' };
|
|
107
|
+
}
|
|
108
|
+
credMeta.counter = authenticationInfo.newCounter;
|
|
109
|
+
credMeta.last_used_at = new Date().toISOString();
|
|
110
|
+
yield this.adminforth.resource(this.options.passkeys.credentialResourceID).update(cred[this.options.passkeys.credentialIdFieldName], { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(credMeta) });
|
|
111
|
+
return { ok: true, passkeyConfirmed: true };
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
return { ok: false, error: 'Error authenticating passkey: ' + e };
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
46
118
|
modifyResourceConfig(adminforth, resourceConfig) {
|
|
119
|
+
var _a;
|
|
47
120
|
super.modifyResourceConfig(adminforth, resourceConfig);
|
|
48
121
|
this.adminforth = adminforth;
|
|
49
122
|
this.adminForthAuth = adminforth.auth;
|
|
123
|
+
const suggestionPeriod = this.parsePeriod(((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.suggestionPeriod) || "5d");
|
|
124
|
+
const isPasskeysEnabled = this.options.passkeys ? true : false;
|
|
50
125
|
const customPages = this.adminforth.config.customization.customPages;
|
|
51
126
|
customPages.push({
|
|
52
127
|
path: '/confirm2fa',
|
|
53
|
-
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { customLayout: true } }
|
|
128
|
+
component: { file: this.componentPath('TwoFactorsConfirmation.vue'), meta: { customLayout: true, suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
54
129
|
});
|
|
55
130
|
customPages.push({
|
|
56
131
|
path: '/setup2fa',
|
|
57
|
-
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true } }
|
|
132
|
+
component: { file: this.componentPath('TwoFactorsSetup.vue'), meta: { title: 'Setup 2FA', customLayout: true, suggestionPeriod: suggestionPeriod, isPasskeysEnabled: isPasskeysEnabled } }
|
|
58
133
|
});
|
|
59
134
|
const everyPageBottomInjections = this.adminforth.config.customization.globalInjections.everyPageBottom || [];
|
|
60
135
|
everyPageBottomInjections.push({ file: this.componentPath('TwoFAModal.vue'), meta: {} });
|
|
61
136
|
this.activate(resourceConfig, adminforth);
|
|
137
|
+
if (this.options.passkeys) {
|
|
138
|
+
if (!this.adminforth.config.auth.userMenuSettingsPages) {
|
|
139
|
+
this.adminforth.config.auth.userMenuSettingsPages = [];
|
|
140
|
+
}
|
|
141
|
+
this.adminforth.config.auth.userMenuSettingsPages.push({
|
|
142
|
+
icon: 'flowbite:lock-solid',
|
|
143
|
+
pageLabel: 'Passkeys',
|
|
144
|
+
slug: 'passkeys',
|
|
145
|
+
component: this.componentPath('TwoFactorsPasskeysSettings.vue'),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
validateConfigAfterDiscover(adminforth, resourceConfig) {
|
|
150
|
+
if (this.options.passkeys) {
|
|
151
|
+
const adminForthResources = [];
|
|
152
|
+
for (const res of adminforth.config.resources) {
|
|
153
|
+
adminForthResources.push(res.resourceId);
|
|
154
|
+
}
|
|
155
|
+
if (!this.options.passkeys.credentialResourceID) {
|
|
156
|
+
throw new Error('Passkeys credentialResourceID is required');
|
|
157
|
+
}
|
|
158
|
+
if (!(adminForthResources.includes(this.options.passkeys.credentialResourceID))) {
|
|
159
|
+
throw new Error('Passkeys credentialResourceID is not valid');
|
|
160
|
+
}
|
|
161
|
+
if (!this.options.passkeys.credentialIdFieldName) {
|
|
162
|
+
throw new Error('Passkeys credentialIdFieldName is required');
|
|
163
|
+
}
|
|
164
|
+
if (!this.options.passkeys.credentialMetaFieldName) {
|
|
165
|
+
throw new Error('Passkeys credentialMetaFieldName is required');
|
|
166
|
+
}
|
|
167
|
+
if (!this.options.passkeys.credentialUserIdFieldName) {
|
|
168
|
+
throw new Error('Passkeys credentialUserIdFieldName is required');
|
|
169
|
+
}
|
|
170
|
+
if (!this.options.passkeys.settings) {
|
|
171
|
+
throw new Error('Passkeys settings are required when passkeys option is enabled');
|
|
172
|
+
}
|
|
173
|
+
if (!this.options.passkeys.settings.expectedOrigin) {
|
|
174
|
+
throw new Error('Passkeys settings.expectedOrigin is required');
|
|
175
|
+
}
|
|
176
|
+
const origin = new URL(this.options.passkeys.settings.expectedOrigin).origin;
|
|
177
|
+
if (origin !== this.options.passkeys.settings.expectedOrigin) {
|
|
178
|
+
throw new Error('Passkeys settings.expectedOrigin is not valid');
|
|
179
|
+
}
|
|
180
|
+
if (this.options.passkeys.settings.authenticatorSelection) {
|
|
181
|
+
if (this.options.passkeys.settings.authenticatorSelection.authenticatorAttachment) {
|
|
182
|
+
if (!['platform', 'cross-platform', 'both'].includes(this.options.passkeys.settings.authenticatorSelection.authenticatorAttachment)) {
|
|
183
|
+
throw new Error('Passkeys settings.authenticatorSelection.authenticatorAttachment is not valid');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (this.options.passkeys.settings.authenticatorSelection.userVerification) {
|
|
187
|
+
if (!['required', 'discouraged'].includes(this.options.passkeys.settings.authenticatorSelection.userVerification)) {
|
|
188
|
+
throw new Error('Passkeys settings.authenticatorSelection.userVerification is not valid');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (!this.options.passkeys.settings.user) {
|
|
193
|
+
throw new Error('Passkeys settings.user is required');
|
|
194
|
+
}
|
|
195
|
+
if (!this.options.passkeys.settings.user.nameField) {
|
|
196
|
+
throw new Error('Passkeys settings.user.nameField is required');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
62
199
|
}
|
|
63
200
|
activate(resourceConfig, adminforth) {
|
|
64
201
|
if (!this.options.twoFaSecretFieldName) {
|
|
@@ -83,8 +220,8 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
83
220
|
const userName = adminUser.dbUser[adminforth.config.auth.usernameField];
|
|
84
221
|
const brandName = adminforth.config.customization.brandName;
|
|
85
222
|
const brandNameSlug = adminforth.config.customization.brandNameSlug;
|
|
86
|
-
const issuerName = (this.options.
|
|
87
|
-
? this.options.
|
|
223
|
+
const issuerName = (this.options.customBrandPrefix && this.options.customBrandPrefix.trim())
|
|
224
|
+
? this.options.customBrandPrefix.trim()
|
|
88
225
|
: brandName;
|
|
89
226
|
const authResource = adminforth.config.resources.find((res) => res.resourceId === adminforth.config.auth.usersResourceId);
|
|
90
227
|
const authPk = authResource.columns.find((col) => col.primaryKey).name;
|
|
@@ -100,10 +237,19 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
100
237
|
if (!secret) {
|
|
101
238
|
const tempSecret = twofactor.generateSecret({ name: issuerName, account: userName });
|
|
102
239
|
newSecret = tempSecret.secret;
|
|
240
|
+
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'temp2FA', '2h');
|
|
241
|
+
this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: totpTemporaryJWT, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
|
|
242
|
+
return {
|
|
243
|
+
body: {
|
|
244
|
+
loginAllowed: false,
|
|
245
|
+
redirectTo: secret ? '/confirm2fa' : '/setup2fa',
|
|
246
|
+
},
|
|
247
|
+
ok: true
|
|
248
|
+
};
|
|
103
249
|
}
|
|
104
250
|
else {
|
|
105
|
-
const value = this.adminforth.auth.issueJWT({ userName, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, '
|
|
106
|
-
|
|
251
|
+
const value = this.adminforth.auth.issueJWT({ userName, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'temp2FA', '2h');
|
|
252
|
+
this.adminforth.auth.setCustomCookie({ response, payload: { name: "2FaTemporaryJWT", value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
|
|
107
253
|
return {
|
|
108
254
|
body: {
|
|
109
255
|
loginAllowed: false,
|
|
@@ -112,15 +258,6 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
112
258
|
ok: true
|
|
113
259
|
};
|
|
114
260
|
}
|
|
115
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: issuerName, pk: userPk, userCanSkipSetup, rememberMeDays }, 'tempTotp', '2h');
|
|
116
|
-
response.setHeader('Set-Cookie', `adminforth_${brandNameSlug}_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
|
|
117
|
-
return {
|
|
118
|
-
body: {
|
|
119
|
-
loginAllowed: false,
|
|
120
|
-
redirectTo: secret ? '/confirm2fa' : '/setup2fa',
|
|
121
|
-
},
|
|
122
|
-
ok: true
|
|
123
|
-
};
|
|
124
261
|
}));
|
|
125
262
|
}
|
|
126
263
|
setupEndpoints(server) {
|
|
@@ -129,28 +266,28 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
129
266
|
path: `/plugin/twofa/initSetup`,
|
|
130
267
|
noAuth: true,
|
|
131
268
|
handler: (server) => __awaiter(this, void 0, void 0, function* () {
|
|
132
|
-
var _a;
|
|
133
269
|
const toReturn = { totpJWT: null, status: 'ok', };
|
|
134
|
-
const
|
|
135
|
-
const totpTemporaryJWT = (_a = server.cookies.find((cookie) => cookie.key === `adminforth_${brandNameSlug}_totpTemporaryJWT`)) === null || _a === void 0 ? void 0 : _a.value;
|
|
270
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: server.cookies, name: "2FaTemporaryJWT" });
|
|
136
271
|
if (totpTemporaryJWT) {
|
|
137
272
|
toReturn.totpJWT = totpTemporaryJWT;
|
|
138
273
|
}
|
|
139
274
|
return toReturn;
|
|
140
275
|
})
|
|
141
276
|
});
|
|
277
|
+
// this enpoints sets final login cookie if 2nd factor is confirmed
|
|
142
278
|
server.endpoint({
|
|
143
279
|
method: 'POST',
|
|
144
|
-
path: `/plugin/twofa/
|
|
280
|
+
path: `/plugin/twofa/confirmLogin`,
|
|
145
281
|
noAuth: true,
|
|
146
282
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response, cookies }) {
|
|
147
|
-
var _b;
|
|
148
283
|
const brandNameSlug = this.adminforth.config.customization.brandNameSlug;
|
|
149
|
-
const totpTemporaryJWT = (
|
|
150
|
-
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, '
|
|
151
|
-
if (!decoded)
|
|
284
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
|
|
285
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
|
|
286
|
+
if (!decoded) {
|
|
152
287
|
return { status: 'error', message: 'Invalid token' };
|
|
288
|
+
}
|
|
153
289
|
if (decoded.newSecret) {
|
|
290
|
+
// set up standard TOTP request - ensured by presence of newSecret in temp2FA token
|
|
154
291
|
const verified = body.skip && decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code, this.options.timeStepWindow);
|
|
155
292
|
if (verified) {
|
|
156
293
|
this.connectors = this.adminforth.connectors;
|
|
@@ -158,7 +295,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
158
295
|
const connector = this.connectors[this.authResource.dataSource];
|
|
159
296
|
yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
|
|
160
297
|
}
|
|
161
|
-
this.adminforth.auth.removeCustomCookie({ response, name: '
|
|
298
|
+
this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
|
|
162
299
|
this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
|
|
163
300
|
return { status: 'ok', allowedLogin: true };
|
|
164
301
|
}
|
|
@@ -167,13 +304,32 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
167
304
|
}
|
|
168
305
|
}
|
|
169
306
|
else {
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
307
|
+
// login with confirming existing TOTP or Passkey
|
|
308
|
+
let verified = null;
|
|
309
|
+
if (body.usePasskey && this.options.passkeys) {
|
|
310
|
+
// passkeys are enabled and user wants to use them
|
|
311
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: `passkeyTemporaryJWT` });
|
|
312
|
+
if (!passkeysCookies) {
|
|
313
|
+
return { error: 'Passkey token is required' };
|
|
314
|
+
}
|
|
315
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
|
|
316
|
+
if (!decodedPasskeysCookies) {
|
|
317
|
+
return { error: 'Invalid passkey token' };
|
|
318
|
+
}
|
|
319
|
+
const res = yield this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies);
|
|
320
|
+
if (res.ok && res.passkeyConfirmed) {
|
|
321
|
+
verified = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
// user already has TOTP secret, get it
|
|
326
|
+
this.connectors = this.adminforth.connectors;
|
|
327
|
+
const connector = this.connectors[this.authResource.dataSource];
|
|
328
|
+
const user = yield connector.getRecordByPrimaryKey(this.authResource, decoded.pk);
|
|
329
|
+
verified = twofactor.verifyToken(user[this.options.twoFaSecretFieldName], body.code, this.options.timeStepWindow);
|
|
330
|
+
}
|
|
175
331
|
if (verified) {
|
|
176
|
-
this.adminforth.auth.removeCustomCookie({ response, name: '
|
|
332
|
+
this.adminforth.auth.removeCustomCookie({ response, name: '2FaTemporaryJWT' });
|
|
177
333
|
this.adminforth.auth.setAuthCookie({ expireInDays: decoded.rememberMeDays, response, username: decoded.userName, pk: decoded.pk });
|
|
178
334
|
return { status: 'ok', allowedLogin: true };
|
|
179
335
|
}
|
|
@@ -188,10 +344,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
188
344
|
path: "/plugin/twofa/skip-allow",
|
|
189
345
|
noAuth: true,
|
|
190
346
|
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
347
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({
|
|
348
|
+
cookies,
|
|
349
|
+
name: `2FaTemporaryJWT`,
|
|
350
|
+
});
|
|
351
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "temp2FA");
|
|
195
352
|
if (!decoded) {
|
|
196
353
|
return { status: "error", message: "Invalid token" };
|
|
197
354
|
}
|
|
@@ -225,5 +382,262 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
225
382
|
return verified ? { ok: true } : { error: 'Wrong or expired OTP code' };
|
|
226
383
|
})
|
|
227
384
|
});
|
|
385
|
+
server.endpoint({
|
|
386
|
+
method: 'POST',
|
|
387
|
+
path: `/plugin/passkeys/registerPasskeyRequest`,
|
|
388
|
+
noAuth: false,
|
|
389
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, response }) {
|
|
390
|
+
var _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
391
|
+
const mode = body === null || body === void 0 ? void 0 : body.mode;
|
|
392
|
+
const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
|
|
393
|
+
const rp = {
|
|
394
|
+
name: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.rp.name) || this.adminforth.config.customization.brandName,
|
|
395
|
+
id: ((_f = (_e = (_d = this.options.passkeys) === null || _d === void 0 ? void 0 : _d.settings) === null || _e === void 0 ? void 0 : _e.rp) === null || _f === void 0 ? void 0 : _f.id) || (new URL(settingsOrigin)).hostname,
|
|
396
|
+
};
|
|
397
|
+
const userResourceId = this.adminforth.config.auth.usersResourceId;
|
|
398
|
+
const usersResource = this.adminforth.config.resources.find(r => r.resourceId === userResourceId);
|
|
399
|
+
const usersPrimaryKeyColumn = usersResource.columns.find((col) => col.primaryKey);
|
|
400
|
+
const usersPrimaryKeyFieldName = usersPrimaryKeyColumn.name;
|
|
401
|
+
const userInfo = yield this.adminforth.resource(userResourceId).get([Filters.EQ(usersPrimaryKeyFieldName, adminUser.pk)]);
|
|
402
|
+
const user = {
|
|
403
|
+
id: adminUser.pk,
|
|
404
|
+
name: userInfo[(_g = this.options.passkeys) === null || _g === void 0 ? void 0 : _g.settings.user.nameField],
|
|
405
|
+
displayName: userInfo[(_h = this.options.passkeys) === null || _h === void 0 ? void 0 : _h.settings.user.displayNameField] ? userInfo[(_j = this.options.passkeys) === null || _j === void 0 ? void 0 : _j.settings.user.displayNameField] : userInfo[(_k = this.options.passkeys) === null || _k === void 0 ? void 0 : _k.settings.user.nameField],
|
|
406
|
+
};
|
|
407
|
+
const excludeCredentials = [];
|
|
408
|
+
const temp = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
409
|
+
for (const rec of temp) {
|
|
410
|
+
if (rec.credential_id && rec.credential_id.length > 0) {
|
|
411
|
+
const meta = JSON.parse(rec[this.options.passkeys.credentialMetaFieldName]);
|
|
412
|
+
excludeCredentials.push({
|
|
413
|
+
id: rec.credential_id,
|
|
414
|
+
type: "public-key",
|
|
415
|
+
transports: JSON.parse(meta.transports || '[]')
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const options = yield generateRegistrationOptions({
|
|
420
|
+
rpName: rp.name,
|
|
421
|
+
rpID: rp.id,
|
|
422
|
+
userID: isoUint8Array.fromUTF8String(user.id),
|
|
423
|
+
userName: user.name,
|
|
424
|
+
userDisplayName: user.displayName,
|
|
425
|
+
excludeCredentials,
|
|
426
|
+
authenticatorSelection: {
|
|
427
|
+
authenticatorAttachment: mode,
|
|
428
|
+
requireResidentKey: ((_l = this.options.passkeys) === null || _l === void 0 ? void 0 : _l.settings.authenticatorSelection.requireResidentKey) || true,
|
|
429
|
+
userVerification: ((_m = this.options.passkeys) === null || _m === void 0 ? void 0 : _m.settings.authenticatorSelection.userVerification) || "required"
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '5m');
|
|
433
|
+
this.adminforth.auth.setCustomCookie({ response, payload: { name: "passkeyTemporaryJWT", value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
|
|
434
|
+
return { ok: true, data: options };
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
server.endpoint({
|
|
438
|
+
method: 'POST',
|
|
439
|
+
path: `/plugin/passkeys/finishRegisteringPasskey`,
|
|
440
|
+
noAuth: false,
|
|
441
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser, cookies }) {
|
|
442
|
+
var _b, _c, _d, _e, _f;
|
|
443
|
+
const passkeysCookies = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "passkeyTemporaryJWT" });
|
|
444
|
+
if (!passkeysCookies) {
|
|
445
|
+
return { error: 'Passkey token is required' };
|
|
446
|
+
}
|
|
447
|
+
const decodedPasskeysCookies = yield this.adminforth.auth.verify(passkeysCookies, 'tempPasskeyChallenge', false);
|
|
448
|
+
if (!decodedPasskeysCookies) {
|
|
449
|
+
return { error: 'Invalid passkey token' };
|
|
450
|
+
}
|
|
451
|
+
const settingsOrigin = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.expectedOrigin;
|
|
452
|
+
const expectedOrigin = body.origin;
|
|
453
|
+
const expectedChallenge = decodedPasskeysCookies.challenge;
|
|
454
|
+
const expectedRPID = ((_e = (_d = (_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings) === null || _d === void 0 ? void 0 : _d.rp) === null || _e === void 0 ? void 0 : _e.id) || (new URL(settingsOrigin)).hostname;
|
|
455
|
+
const response = JSON.parse(body.credential);
|
|
456
|
+
try {
|
|
457
|
+
if (settingsOrigin !== expectedOrigin) {
|
|
458
|
+
throw new Error('Invalid origin');
|
|
459
|
+
}
|
|
460
|
+
const { verified, registrationInfo } = yield verifyRegistrationResponse({
|
|
461
|
+
response,
|
|
462
|
+
expectedChallenge,
|
|
463
|
+
expectedOrigin,
|
|
464
|
+
expectedRPID,
|
|
465
|
+
requireUserVerification: ((_f = this.options.passkeys) === null || _f === void 0 ? void 0 : _f.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
466
|
+
});
|
|
467
|
+
if (!verified) {
|
|
468
|
+
throw new Error('Verification failed.');
|
|
469
|
+
}
|
|
470
|
+
const { aaguid, credential, credentialBackedUp } = registrationInfo;
|
|
471
|
+
const credentialPublicKey = credential.publicKey;
|
|
472
|
+
const credentialID = credential.id;
|
|
473
|
+
const base64CredentialID = credentialID;
|
|
474
|
+
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
|
|
475
|
+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
476
|
+
if (!credResource) {
|
|
477
|
+
throw new Error('Credential resource not found.');
|
|
478
|
+
}
|
|
479
|
+
yield this.adminforth.createResourceRecord({
|
|
480
|
+
resource: credResource,
|
|
481
|
+
record: {
|
|
482
|
+
[this.options.passkeys.credentialIdFieldName]: base64CredentialID,
|
|
483
|
+
[this.options.passkeys.credentialUserIdFieldName]: adminUser.pk,
|
|
484
|
+
[this.options.passkeys.credentialMetaFieldName]: JSON.stringify({
|
|
485
|
+
public_key: base64PublicKey,
|
|
486
|
+
public_key_algorithm: response.response.publicKeyAlgorithm,
|
|
487
|
+
sign_count: 0,
|
|
488
|
+
transports: JSON.stringify(response.response.transports),
|
|
489
|
+
created_at: new Date().toISOString(),
|
|
490
|
+
last_used_at: new Date().toISOString(),
|
|
491
|
+
aaguid: aaguid,
|
|
492
|
+
name: `Passkey ${adminUser.username}`,
|
|
493
|
+
}),
|
|
494
|
+
},
|
|
495
|
+
adminUser: adminUser
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
console.error(error);
|
|
500
|
+
return { error: 'Error registering passkey: ' + error.message };
|
|
501
|
+
}
|
|
502
|
+
return { ok: true };
|
|
503
|
+
})
|
|
504
|
+
});
|
|
505
|
+
server.endpoint({
|
|
506
|
+
method: 'POST',
|
|
507
|
+
path: `/plugin/passkeys/signInRequest`,
|
|
508
|
+
noAuth: true,
|
|
509
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ response }) {
|
|
510
|
+
var _b, _c;
|
|
511
|
+
try {
|
|
512
|
+
const options = yield generateAuthenticationOptions({
|
|
513
|
+
rpID: (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp.id,
|
|
514
|
+
userVerification: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.userVerification) || "required"
|
|
515
|
+
});
|
|
516
|
+
const value = this.adminforth.auth.issueJWT({ "challenge": options.challenge }, 'tempPasskeyChallenge', '5m');
|
|
517
|
+
this.adminforth.auth.setCustomCookie({ response, payload: { name: `passkeyTemporaryJWT`, value: value, expiry: 1 * 60 * 60 * 1000, httpOnly: true } });
|
|
518
|
+
return { ok: true, data: options };
|
|
519
|
+
}
|
|
520
|
+
catch (e) {
|
|
521
|
+
return { ok: false, error: e };
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
});
|
|
525
|
+
server.endpoint({
|
|
526
|
+
method: 'GET',
|
|
527
|
+
path: `/plugin/passkeys/getPasskeys`,
|
|
528
|
+
noAuth: false,
|
|
529
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ adminUser }) {
|
|
530
|
+
var _b;
|
|
531
|
+
let passkeys;
|
|
532
|
+
try {
|
|
533
|
+
passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
return { ok: false, error: 'Error fetching passkeys: ' + error.message };
|
|
537
|
+
}
|
|
538
|
+
let dataToReturn = [];
|
|
539
|
+
for (const pk of passkeys) {
|
|
540
|
+
const parsedKey = JSON.parse(pk[this.options.passkeys.credentialMetaFieldName]);
|
|
541
|
+
dataToReturn.push({
|
|
542
|
+
name: parsedKey.name,
|
|
543
|
+
created_at: parsedKey.created_at,
|
|
544
|
+
last_used_at: parsedKey.last_used_at,
|
|
545
|
+
id: pk[this.options.passkeys.credentialIdFieldName],
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return { ok: true, data: dataToReturn, authenticatorAttachment: ((_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.authenticatorSelection.authenticatorAttachment) || 'both' };
|
|
549
|
+
})
|
|
550
|
+
});
|
|
551
|
+
server.endpoint({
|
|
552
|
+
method: 'DELETE',
|
|
553
|
+
path: `/plugin/passkeys/deletePasskey`,
|
|
554
|
+
noAuth: false,
|
|
555
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
556
|
+
const passkeyId = body.passkeyId;
|
|
557
|
+
if (!passkeyId) {
|
|
558
|
+
return { ok: false, error: 'Passkey ID is required' };
|
|
559
|
+
}
|
|
560
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
561
|
+
if (!passkeyRecord) {
|
|
562
|
+
return { ok: false, error: 'Passkey not found' };
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
566
|
+
if (!credResource) {
|
|
567
|
+
throw new Error('Credential resource not found.');
|
|
568
|
+
}
|
|
569
|
+
yield this.adminforth.deleteResourceRecord({
|
|
570
|
+
resource: credResource,
|
|
571
|
+
recordId: passkeyId,
|
|
572
|
+
record: passkeyRecord,
|
|
573
|
+
adminUser: adminUser,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch (error) {
|
|
577
|
+
return { ok: false, error: 'Error deleting passkey: ' + error.message };
|
|
578
|
+
}
|
|
579
|
+
return { ok: true };
|
|
580
|
+
})
|
|
581
|
+
});
|
|
582
|
+
server.endpoint({
|
|
583
|
+
method: 'POST',
|
|
584
|
+
path: `/plugin/passkeys/renamePasskey`,
|
|
585
|
+
noAuth: false,
|
|
586
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ body, adminUser }) {
|
|
587
|
+
const passkeyId = body.passkeyId;
|
|
588
|
+
const newName = body.newName;
|
|
589
|
+
if (!passkeyId) {
|
|
590
|
+
return { ok: false, error: 'Passkey ID is required' };
|
|
591
|
+
}
|
|
592
|
+
if (!newName) {
|
|
593
|
+
return { ok: false, error: 'New name is required' };
|
|
594
|
+
}
|
|
595
|
+
const passkeyRecord = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).get([Filters.EQ(this.options.passkeys.credentialIdFieldName, passkeyId), Filters.EQ(this.options.passkeys.credentialUserIdFieldName, adminUser.pk)]);
|
|
596
|
+
if (!passkeyRecord) {
|
|
597
|
+
return { ok: false, error: 'Passkey not found' };
|
|
598
|
+
}
|
|
599
|
+
const meta = JSON.parse(passkeyRecord[this.options.passkeys.credentialMetaFieldName]);
|
|
600
|
+
meta.name = newName;
|
|
601
|
+
const newRecord = Object.assign(Object.assign({}, passkeyRecord), { [this.options.passkeys.credentialMetaFieldName]: JSON.stringify(meta) });
|
|
602
|
+
try {
|
|
603
|
+
const credResource = this.adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID);
|
|
604
|
+
if (!credResource) {
|
|
605
|
+
throw new Error('Credential resource not found.');
|
|
606
|
+
}
|
|
607
|
+
yield this.adminforth.updateResourceRecord({
|
|
608
|
+
resource: credResource,
|
|
609
|
+
recordId: passkeyId,
|
|
610
|
+
oldRecord: passkeyRecord,
|
|
611
|
+
record: newRecord,
|
|
612
|
+
adminUser: adminUser
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
return { ok: false, error: 'Error renaming passkey: ' + error.message };
|
|
617
|
+
}
|
|
618
|
+
return { ok: true };
|
|
619
|
+
})
|
|
620
|
+
});
|
|
621
|
+
server.endpoint({
|
|
622
|
+
method: 'POST',
|
|
623
|
+
path: `/plugin/passkeys/checkIfUserHasPasskeys`,
|
|
624
|
+
noAuth: true,
|
|
625
|
+
handler: (_a) => __awaiter(this, [_a], void 0, function* ({ cookies }) {
|
|
626
|
+
const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({ cookies: cookies, name: "2FaTemporaryJWT" });
|
|
627
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, 'temp2FA');
|
|
628
|
+
if (!decoded)
|
|
629
|
+
return { ok: false, error: 'Invalid token' };
|
|
630
|
+
if (decoded.newSecret) {
|
|
631
|
+
return { ok: true, hasPasskeys: false };
|
|
632
|
+
}
|
|
633
|
+
const passkeys = yield this.adminforth.resource(this.options.passkeys.credentialResourceID).list([Filters.EQ(this.options.passkeys.credentialUserIdFieldName, decoded.pk)]);
|
|
634
|
+
if (passkeys && passkeys.length > 0) {
|
|
635
|
+
return { ok: true, hasPasskeys: true };
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
return { ok: true, hasPasskeys: false };
|
|
639
|
+
}
|
|
640
|
+
})
|
|
641
|
+
});
|
|
228
642
|
}
|
|
229
643
|
}
|