@adminforth/two-factors-auth 3.3.10 → 4.0.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/build.log +2 -2
- package/custom/TwoFAModal.vue +4 -4
- package/custom/TwoFactorsConfirmation.vue +9 -10
- package/custom/TwoFactorsPasskeysSettings.vue +10 -10
- package/custom/utils.js +7 -8
- package/dist/custom/TwoFAModal.vue +4 -4
- package/dist/custom/TwoFactorsConfirmation.vue +9 -10
- package/dist/custom/TwoFactorsPasskeysSettings.vue +10 -10
- package/dist/custom/utils.js +7 -8
- package/dist/endpoints/registerPasskeyEndpoints.d.ts +2 -0
- package/dist/endpoints/registerPasskeyEndpoints.js +59 -0
- package/dist/endpoints/registerTwoFaEndpoints.d.ts +2 -0
- package/dist/endpoints/registerTwoFaEndpoints.js +46 -0
- package/dist/handlers/createPasskeyHandlers.d.ts +48 -0
- package/dist/handlers/createPasskeyHandlers.js +99 -0
- package/dist/handlers/createTwoFaHandlers.d.ts +56 -0
- package/dist/handlers/createTwoFaHandlers.js +167 -0
- package/dist/index.d.ts +13 -27
- package/dist/index.js +51 -858
- package/dist/repositories/passkeyRepository.d.ts +18 -0
- package/dist/repositories/passkeyRepository.js +89 -0
- package/dist/repositories/userRepository.d.ts +14 -0
- package/dist/repositories/userRepository.js +45 -0
- package/dist/services/cookieService.d.ts +52 -0
- package/dist/services/cookieService.js +144 -0
- package/dist/services/passkeyService.d.ts +84 -0
- package/dist/services/passkeyService.js +369 -0
- package/dist/services/totpService.d.ts +21 -0
- package/dist/services/totpService.js +43 -0
- package/dist/services/verifyMfaConfirmation.d.ts +30 -0
- package/dist/services/verifyMfaConfirmation.js +67 -0
- package/dist/utils/errors.d.ts +4 -0
- package/dist/utils/errors.js +9 -0
- package/dist/utils/parsePeriod.d.ts +1 -0
- package/dist/utils/parsePeriod.js +17 -0
- package/dist/utils/types.d.ts +13 -0
- package/dist/utils/types.js +1 -0
- package/endpoints/registerPasskeyEndpoints.ts +61 -0
- package/endpoints/registerTwoFaEndpoints.ts +45 -0
- package/handlers/createPasskeyHandlers.ts +103 -0
- package/handlers/createTwoFaHandlers.ts +172 -0
- package/index.ts +57 -886
- package/package.json +3 -3
- package/repositories/passkeyRepository.ts +78 -0
- package/repositories/userRepository.ts +42 -0
- package/services/cookieService.ts +165 -0
- package/services/passkeyService.ts +375 -0
- package/services/totpService.ts +37 -0
- package/services/verifyMfaConfirmation.ts +92 -0
- package/utils/errors.ts +13 -0
- package/utils/parsePeriod.ts +19 -0
- package/utils/types.ts +17 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { convertPeriodToSeconds } from "adminforth";
|
|
11
|
+
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server';
|
|
12
|
+
import { isoUint8Array, isoBase64URL } from '@simplewebauthn/server/helpers';
|
|
13
|
+
import aaguids from '../custom/aaguid.json';
|
|
14
|
+
import { PasskeyRepository } from "../repositories/passkeyRepository.js";
|
|
15
|
+
import { UserRepository } from "../repositories/userRepository.js";
|
|
16
|
+
import { CookieService } from "./cookieService.js";
|
|
17
|
+
import { errorMessage, errorResult, prefixedErrorResult } from "../utils/errors.js";
|
|
18
|
+
export class PasskeyService {
|
|
19
|
+
constructor(options, adminforth, userRepository = new UserRepository(adminforth, options), passkeyRepository = new PasskeyRepository(adminforth, options), cookieService = new CookieService(adminforth, options)) {
|
|
20
|
+
this.options = options;
|
|
21
|
+
this.adminforth = adminforth;
|
|
22
|
+
this.userRepository = userRepository;
|
|
23
|
+
this.passkeyRepository = passkeyRepository;
|
|
24
|
+
this.cookieService = cookieService;
|
|
25
|
+
}
|
|
26
|
+
markChallengeAsUsed(challenge, expiresIn) {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const expiresInSeconds = expiresIn ? convertPeriodToSeconds(expiresIn) : undefined;
|
|
29
|
+
yield this.options.passkeys.keyValueAdapter.set(challenge, 'stub_value', expiresInSeconds);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
isChallengeUnused(challenge) {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const res = yield this.options.passkeys.keyValueAdapter.get(challenge);
|
|
35
|
+
return !res;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
validateLoginChallengeCookie(cookies) {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
var _a;
|
|
41
|
+
const passkeysCookies = this.cookieService.getPasskeyLoginTemporary(cookies);
|
|
42
|
+
if (!passkeysCookies) {
|
|
43
|
+
return errorResult('Passkey token is required');
|
|
44
|
+
}
|
|
45
|
+
const decodedPasskeysCookies = yield this.cookieService.verifyPasskeyLoginTemporary(cookies);
|
|
46
|
+
if (!(decodedPasskeysCookies === null || decodedPasskeysCookies === void 0 ? void 0 : decodedPasskeysCookies.challenge)) {
|
|
47
|
+
return errorResult('Invalid passkey');
|
|
48
|
+
}
|
|
49
|
+
const isChallangeValid = yield this.isChallengeUnused(decodedPasskeysCookies.challenge);
|
|
50
|
+
if (isChallangeValid) {
|
|
51
|
+
yield this.markChallengeAsUsed(decodedPasskeysCookies.challenge, ((_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.challengeValidityPeriod) || '2m');
|
|
52
|
+
}
|
|
53
|
+
if (!decodedPasskeysCookies || !isChallangeValid) {
|
|
54
|
+
return errorResult('Invalid passkey');
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, decodedPasskeysCookies };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
verifyPasskeyResponse(body, user_id, cookies) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
var _a, _b, _c, _d, _e, _f;
|
|
62
|
+
const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
|
|
63
|
+
const expectedOrigin = body.origin;
|
|
64
|
+
const expectedChallenge = cookies.challenge;
|
|
65
|
+
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;
|
|
66
|
+
try {
|
|
67
|
+
const response = JSON.parse(body.response);
|
|
68
|
+
if (settingsOrigin !== expectedOrigin) {
|
|
69
|
+
throw new Error(`Origin mismatch. Allowed in settings: ${settingsOrigin}, received from client: ${expectedOrigin}`);
|
|
70
|
+
}
|
|
71
|
+
const cred = yield this.passkeyRepository.getByCredentialId(response.id);
|
|
72
|
+
if (!cred) {
|
|
73
|
+
throw new Error('Credential not found.');
|
|
74
|
+
}
|
|
75
|
+
const credMeta = cred[this.options.passkeys.credentialMetaFieldName];
|
|
76
|
+
if (!credMeta || !credMeta.public_key) {
|
|
77
|
+
throw new Error('Credential public key not found.');
|
|
78
|
+
}
|
|
79
|
+
const usersPrimaryKeyFieldName = this.userRepository.getUserPkField();
|
|
80
|
+
const user = yield this.userRepository.getAuthUser(cred[this.options.passkeys.credentialUserIdFieldName]);
|
|
81
|
+
if (!user || !user_id || user[usersPrimaryKeyFieldName] !== user_id) {
|
|
82
|
+
throw new Error('User not found.');
|
|
83
|
+
}
|
|
84
|
+
const counter = (_e = credMeta.sign_count) !== null && _e !== void 0 ? _e : 0;
|
|
85
|
+
const { verified, authenticationInfo } = yield verifyAuthenticationResponse({
|
|
86
|
+
response,
|
|
87
|
+
expectedChallenge,
|
|
88
|
+
expectedOrigin: settingsOrigin,
|
|
89
|
+
expectedRPID,
|
|
90
|
+
credential: {
|
|
91
|
+
id: cred[this.options.passkeys.credentialIdFieldName],
|
|
92
|
+
publicKey: isoBase64URL.toBuffer(credMeta.public_key),
|
|
93
|
+
counter,
|
|
94
|
+
transports: credMeta.transports,
|
|
95
|
+
},
|
|
96
|
+
requireUserVerification: ((_f = this.options.passkeys) === null || _f === void 0 ? void 0 : _f.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
97
|
+
});
|
|
98
|
+
if (!verified) {
|
|
99
|
+
return errorResult('User verification failed.');
|
|
100
|
+
}
|
|
101
|
+
credMeta.sign_count = authenticationInfo.newCounter;
|
|
102
|
+
credMeta.last_used_at = new Date().toISOString();
|
|
103
|
+
yield this.passkeyRepository.updateMeta(cred, credMeta);
|
|
104
|
+
return { ok: true, passkeyConfirmed: true };
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
return prefixedErrorResult('Error authenticating passkey: ', e);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
createRegistrationOptions(mode, adminUser, response) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
114
|
+
const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
|
|
115
|
+
const authenticatorAttachment = mode === "both" ? undefined : mode;
|
|
116
|
+
const rp = {
|
|
117
|
+
name: ((_c = (_b = this.options.passkeys) === null || _b === void 0 ? void 0 : _b.settings.rp) === null || _c === void 0 ? void 0 : _c.name) || this.adminforth.config.customization.brandName,
|
|
118
|
+
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,
|
|
119
|
+
};
|
|
120
|
+
const userInfo = yield this.userRepository.getAuthUser(adminUser.pk);
|
|
121
|
+
if (!userInfo) {
|
|
122
|
+
return errorResult('User not found');
|
|
123
|
+
}
|
|
124
|
+
const user = {
|
|
125
|
+
pk: adminUser.pk,
|
|
126
|
+
name: userInfo[(_g = this.options.passkeys) === null || _g === void 0 ? void 0 : _g.settings.user.nameField],
|
|
127
|
+
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],
|
|
128
|
+
};
|
|
129
|
+
const excludeCredentials = [];
|
|
130
|
+
const temp = yield this.passkeyRepository.listByUserId(adminUser.pk);
|
|
131
|
+
for (const rec of temp) {
|
|
132
|
+
const credentialId = rec[this.options.passkeys.credentialIdFieldName];
|
|
133
|
+
if (credentialId) {
|
|
134
|
+
const meta = rec[this.options.passkeys.credentialMetaFieldName];
|
|
135
|
+
excludeCredentials.push({
|
|
136
|
+
id: credentialId,
|
|
137
|
+
type: "public-key",
|
|
138
|
+
transports: meta.transports || []
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const options = yield generateRegistrationOptions({
|
|
143
|
+
rpName: rp.name,
|
|
144
|
+
rpID: rp.id,
|
|
145
|
+
userID: isoUint8Array.fromUTF8String(user.pk),
|
|
146
|
+
userName: user.name,
|
|
147
|
+
userDisplayName: user.displayName,
|
|
148
|
+
excludeCredentials,
|
|
149
|
+
authenticatorSelection: {
|
|
150
|
+
authenticatorAttachment,
|
|
151
|
+
requireResidentKey: (_m = (_l = this.options.passkeys) === null || _l === void 0 ? void 0 : _l.settings.authenticatorSelection.requireResidentKey) !== null && _m !== void 0 ? _m : true,
|
|
152
|
+
userVerification: ((_o = this.options.passkeys) === null || _o === void 0 ? void 0 : _o.settings.authenticatorSelection.userVerification) || "required"
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
this.cookieService.setRegisterPasskeyTemporary(response, { challenge: options.challenge, user_id: adminUser.pk });
|
|
156
|
+
return { ok: true, data: options };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
finishRegistration(body, adminUser, cookies) {
|
|
160
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
161
|
+
var _a, _b, _c, _d, _e, _f;
|
|
162
|
+
const passkeysCookies = this.cookieService.getRegisterPasskeyTemporary(cookies);
|
|
163
|
+
if (!passkeysCookies) {
|
|
164
|
+
return { error: 'Passkey token is required' };
|
|
165
|
+
}
|
|
166
|
+
const decodedPasskeysCookies = yield this.cookieService.verifyRegisterPasskeyTemporary(cookies);
|
|
167
|
+
if (!decodedPasskeysCookies) {
|
|
168
|
+
return { error: 'Invalid passkey token' };
|
|
169
|
+
}
|
|
170
|
+
if (decodedPasskeysCookies.user_id !== adminUser.pk) {
|
|
171
|
+
return { error: 'Invalid user' };
|
|
172
|
+
}
|
|
173
|
+
const secret = this.userRepository.getSecret(adminUser.dbUser);
|
|
174
|
+
if (!secret || secret.length === 0) {
|
|
175
|
+
return { error: 'TOTP must be set up before registering a passkey' };
|
|
176
|
+
}
|
|
177
|
+
const settingsOrigin = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.expectedOrigin;
|
|
178
|
+
const expectedOrigin = body.origin;
|
|
179
|
+
const expectedChallenge = decodedPasskeysCookies.challenge;
|
|
180
|
+
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;
|
|
181
|
+
try {
|
|
182
|
+
const response = JSON.parse(body.credential);
|
|
183
|
+
if (settingsOrigin !== expectedOrigin) {
|
|
184
|
+
throw new Error('Invalid origin');
|
|
185
|
+
}
|
|
186
|
+
const { verified, registrationInfo } = yield verifyRegistrationResponse({
|
|
187
|
+
response,
|
|
188
|
+
expectedChallenge,
|
|
189
|
+
expectedOrigin,
|
|
190
|
+
expectedRPID,
|
|
191
|
+
requireUserVerification: ((_e = this.options.passkeys) === null || _e === void 0 ? void 0 : _e.settings.authenticatorSelection.userVerification) === 'discouraged' ? false : true,
|
|
192
|
+
});
|
|
193
|
+
if (!verified) {
|
|
194
|
+
throw new Error('Verification failed.');
|
|
195
|
+
}
|
|
196
|
+
const { aaguid, credential, } = registrationInfo;
|
|
197
|
+
const provider_name = ((_f = aaguids[aaguid]) === null || _f === void 0 ? void 0 : _f.name) || 'Unknown provider';
|
|
198
|
+
const credentialPublicKey = credential.publicKey;
|
|
199
|
+
const credentialID = credential.id;
|
|
200
|
+
const base64CredentialID = credentialID;
|
|
201
|
+
const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey);
|
|
202
|
+
yield this.passkeyRepository.create({
|
|
203
|
+
[this.options.passkeys.credentialIdFieldName]: base64CredentialID,
|
|
204
|
+
[this.options.passkeys.credentialUserIdFieldName]: adminUser.pk,
|
|
205
|
+
[this.options.passkeys.credentialMetaFieldName]: {
|
|
206
|
+
public_key: base64PublicKey,
|
|
207
|
+
public_key_algorithm: response.response.publicKeyAlgorithm,
|
|
208
|
+
sign_count: 0,
|
|
209
|
+
transports: response.response.transports,
|
|
210
|
+
created_at: new Date().toISOString(),
|
|
211
|
+
last_used_at: new Date().toISOString(),
|
|
212
|
+
aaguid: aaguid,
|
|
213
|
+
name: provider_name,
|
|
214
|
+
},
|
|
215
|
+
}, adminUser);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error(error);
|
|
219
|
+
return { error: 'Error registering passkey: ' + errorMessage(error) };
|
|
220
|
+
}
|
|
221
|
+
return { ok: true };
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
createLoginOptions(response) {
|
|
225
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
226
|
+
var _a, _b, _c, _d;
|
|
227
|
+
try {
|
|
228
|
+
const options = yield generateAuthenticationOptions({
|
|
229
|
+
rpID: ((_b = (_a = this.options.passkeys) === null || _a === void 0 ? void 0 : _a.settings.rp) === null || _b === void 0 ? void 0 : _b.id) || (new URL((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.expectedOrigin)).hostname,
|
|
230
|
+
userVerification: ((_d = this.options.passkeys) === null || _d === void 0 ? void 0 : _d.settings.authenticatorSelection.userVerification) || "required"
|
|
231
|
+
});
|
|
232
|
+
this.cookieService.setPasskeyLoginTemporary(response, options.challenge);
|
|
233
|
+
return { ok: true, data: options };
|
|
234
|
+
}
|
|
235
|
+
catch (e) {
|
|
236
|
+
return { ok: false, error: errorMessage(e) };
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
getPasskeys(adminUser) {
|
|
241
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
242
|
+
var _a, _b, _c;
|
|
243
|
+
let passkeys;
|
|
244
|
+
try {
|
|
245
|
+
passkeys = yield this.passkeyRepository.listByUserId(adminUser.pk);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
return prefixedErrorResult('Error fetching passkeys: ', error);
|
|
249
|
+
}
|
|
250
|
+
let dataToReturn = [];
|
|
251
|
+
for (const pk of passkeys) {
|
|
252
|
+
const parsedKey = pk[this.options.passkeys.credentialMetaFieldName];
|
|
253
|
+
dataToReturn.push({
|
|
254
|
+
name: parsedKey.name,
|
|
255
|
+
light_icon: ((_a = aaguids[parsedKey.aaguid]) === null || _a === void 0 ? void 0 : _a.icon_light) || null,
|
|
256
|
+
dark_icon: ((_b = aaguids[parsedKey.aaguid]) === null || _b === void 0 ? void 0 : _b.icon_dark) || null,
|
|
257
|
+
created_at: parsedKey.created_at,
|
|
258
|
+
last_used_at: parsedKey.last_used_at,
|
|
259
|
+
id: pk[this.options.passkeys.credentialIdFieldName],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
return { ok: true, data: dataToReturn, authenticatorAttachment: ((_c = this.options.passkeys) === null || _c === void 0 ? void 0 : _c.settings.authenticatorSelection.authenticatorAttachment) || 'both' };
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
deletePasskey(credentialID, adminUser) {
|
|
266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
if (!credentialID) {
|
|
268
|
+
return errorResult('Passkey ID is required');
|
|
269
|
+
}
|
|
270
|
+
const passkeyRecord = yield this.passkeyRepository.getByCredentialIdAndUserId(credentialID, adminUser.pk);
|
|
271
|
+
if (!passkeyRecord) {
|
|
272
|
+
return errorResult('Passkey not found');
|
|
273
|
+
}
|
|
274
|
+
try {
|
|
275
|
+
yield this.passkeyRepository.delete(passkeyRecord, adminUser);
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
return prefixedErrorResult('Error deleting passkey: ', error);
|
|
279
|
+
}
|
|
280
|
+
return { ok: true };
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
renamePasskey(credentialID, newName, adminUser) {
|
|
284
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
285
|
+
if (!credentialID) {
|
|
286
|
+
return errorResult('Passkey ID is required');
|
|
287
|
+
}
|
|
288
|
+
if (!newName) {
|
|
289
|
+
return errorResult('New name is required');
|
|
290
|
+
}
|
|
291
|
+
const passkeyRecord = yield this.passkeyRepository.getByCredentialIdAndUserId(credentialID, adminUser.pk);
|
|
292
|
+
if (!passkeyRecord) {
|
|
293
|
+
return errorResult('Passkey not found');
|
|
294
|
+
}
|
|
295
|
+
const metaField = this.options.passkeys.credentialMetaFieldName;
|
|
296
|
+
const oldMeta = passkeyRecord[metaField];
|
|
297
|
+
const newMeta = Object.assign(Object.assign({}, oldMeta), { name: newName });
|
|
298
|
+
const newRecord = Object.assign(Object.assign({}, passkeyRecord), { [metaField]: newMeta });
|
|
299
|
+
try {
|
|
300
|
+
yield this.passkeyRepository.update(passkeyRecord, newRecord, adminUser);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return prefixedErrorResult('Error renaming passkey: ', error);
|
|
304
|
+
}
|
|
305
|
+
return { ok: true };
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
checkIfUserHasPasskeys(cookies) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
310
|
+
if (!this.options.passkeys) {
|
|
311
|
+
return { ok: false, hasPasskeys: false };
|
|
312
|
+
}
|
|
313
|
+
const decoded = yield this.cookieService.verifyTotpTemporary(cookies);
|
|
314
|
+
if (!decoded)
|
|
315
|
+
return errorResult('Invalid token');
|
|
316
|
+
if (decoded.newSecret) {
|
|
317
|
+
return { ok: true, hasPasskeys: false };
|
|
318
|
+
}
|
|
319
|
+
const hasPasskeys = yield this.passkeyRepository.hasByUserId(decoded.pk);
|
|
320
|
+
if (hasPasskeys) {
|
|
321
|
+
return { ok: true, hasPasskeys: true };
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
return { ok: true, hasPasskeys: false };
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
hasPasskeysForUser(userId) {
|
|
329
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
330
|
+
return this.passkeyRepository.hasByUserId(userId);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
getLoginUserByPasskeyResponse(passkeyResponse, cookies) {
|
|
334
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
335
|
+
const cookiesValidationResult = yield this.validateLoginChallengeCookie(cookies);
|
|
336
|
+
if (!cookiesValidationResult.ok) {
|
|
337
|
+
return { error: cookiesValidationResult.error };
|
|
338
|
+
}
|
|
339
|
+
let parsedPasskeyResponse;
|
|
340
|
+
try {
|
|
341
|
+
parsedPasskeyResponse = JSON.parse(passkeyResponse.response);
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
return { error: 'Malformed passkey response' };
|
|
345
|
+
}
|
|
346
|
+
const credential_id = parsedPasskeyResponse.id;
|
|
347
|
+
if (!credential_id) {
|
|
348
|
+
return { error: 'Credential ID is required' };
|
|
349
|
+
}
|
|
350
|
+
const passkeyRecord = yield this.passkeyRepository.getByCredentialId(credential_id);
|
|
351
|
+
if (!passkeyRecord) {
|
|
352
|
+
return { error: 'No such passkey found, most likely it was removed on this website but you still have it on your device' };
|
|
353
|
+
}
|
|
354
|
+
const userPk = passkeyRecord[this.options.passkeys.credentialUserIdFieldName];
|
|
355
|
+
if (!userPk) {
|
|
356
|
+
return { error: 'User ID not found in passkey record' };
|
|
357
|
+
}
|
|
358
|
+
const user = yield this.userRepository.getAuthUser(userPk);
|
|
359
|
+
if (!user) {
|
|
360
|
+
return { error: 'User not found' };
|
|
361
|
+
}
|
|
362
|
+
const verificationResult = yield this.verifyPasskeyResponse(passkeyResponse, userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
363
|
+
if (!verificationResult.ok || !verificationResult.passkeyConfirmed) {
|
|
364
|
+
return { error: 'Passkey verification failed' };
|
|
365
|
+
}
|
|
366
|
+
return { ok: true, user };
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AdminUser } from "adminforth";
|
|
2
|
+
import type { PluginOptions } from "../types.js";
|
|
3
|
+
import { UserRepository } from "../repositories/userRepository.js";
|
|
4
|
+
export declare class TotpService {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private readonly userRepository;
|
|
7
|
+
constructor(options: PluginOptions, userRepository: UserRepository);
|
|
8
|
+
generateSecret(issuerName: string, userName: string): string;
|
|
9
|
+
verifySecret(secret: string, code: string): boolean;
|
|
10
|
+
verifyUserCode(userPk: string, code: string): Promise<{
|
|
11
|
+
ok: true;
|
|
12
|
+
} | {
|
|
13
|
+
error: string;
|
|
14
|
+
}>;
|
|
15
|
+
verifyAdminUserCode(adminUser: AdminUser, code: string): Promise<{
|
|
16
|
+
ok: true;
|
|
17
|
+
} | {
|
|
18
|
+
error: string;
|
|
19
|
+
}>;
|
|
20
|
+
saveSecret(userPk: string, secret: string): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import twofactor from 'node-2fa';
|
|
11
|
+
export class TotpService {
|
|
12
|
+
constructor(options, userRepository) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.userRepository = userRepository;
|
|
15
|
+
}
|
|
16
|
+
generateSecret(issuerName, userName) {
|
|
17
|
+
return twofactor.generateSecret({ name: issuerName, account: userName }).secret;
|
|
18
|
+
}
|
|
19
|
+
verifySecret(secret, code) {
|
|
20
|
+
return Boolean(twofactor.verifyToken(secret, code, this.options.timeStepWindow));
|
|
21
|
+
}
|
|
22
|
+
verifyUserCode(userPk, code) {
|
|
23
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
24
|
+
const user = yield this.userRepository.getAuthUser(userPk);
|
|
25
|
+
if (!user)
|
|
26
|
+
return { error: "User not found" };
|
|
27
|
+
const secret = this.userRepository.getSecret(user);
|
|
28
|
+
if (!secret)
|
|
29
|
+
return { error: "2FA is not set up for this user" };
|
|
30
|
+
return this.verifySecret(secret, code) ? { ok: true } : { error: "Wrong or expired OTP code" };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
verifyAdminUserCode(adminUser, code) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
return this.verifyUserCode(this.userRepository.getUserPk(adminUser), code);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
saveSecret(userPk, secret) {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
yield this.userRepository.updateSecret(userPk, secret);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AdminUser, HttpExtra, IAdminForthHttpResponse } from "adminforth";
|
|
2
|
+
import type { CookieList } from "../utils/types.js";
|
|
3
|
+
type TotpConfirmationResult = {
|
|
4
|
+
mode: "totp";
|
|
5
|
+
result: string;
|
|
6
|
+
};
|
|
7
|
+
type PasskeyConfirmationResult = {
|
|
8
|
+
mode: "passkey";
|
|
9
|
+
result: {
|
|
10
|
+
response: string;
|
|
11
|
+
origin: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
type GraceConfirmationResult = {
|
|
15
|
+
mode?: undefined;
|
|
16
|
+
};
|
|
17
|
+
export type MfaConfirmationResult = TotpConfirmationResult | PasskeyConfirmationResult | GraceConfirmationResult;
|
|
18
|
+
export type VerifyOptions = {
|
|
19
|
+
adminUser: AdminUser;
|
|
20
|
+
userPk: string;
|
|
21
|
+
cookies?: CookieList;
|
|
22
|
+
response?: IAdminForthHttpResponse;
|
|
23
|
+
extra?: HttpExtra;
|
|
24
|
+
};
|
|
25
|
+
export declare function verifyMfaConfirmation(ctx: any, confirmationResult: MfaConfirmationResult, opts: VerifyOptions): Promise<{
|
|
26
|
+
ok: true;
|
|
27
|
+
} | {
|
|
28
|
+
error: string;
|
|
29
|
+
}>;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
export function verifyMfaConfirmation(ctx, confirmationResult, opts) {
|
|
11
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
12
|
+
var _a, _b, _c, _d, _e;
|
|
13
|
+
if (!confirmationResult)
|
|
14
|
+
return { error: "Confirmation result is required" };
|
|
15
|
+
const cookies = ((_a = opts.extra) === null || _a === void 0 ? void 0 : _a.cookies) || opts.cookies;
|
|
16
|
+
const response = ((_b = opts.extra) === null || _b === void 0 ? void 0 : _b.response) || opts.response;
|
|
17
|
+
if (ctx.options.usersFilterToApply) {
|
|
18
|
+
const res = ctx.options.usersFilterToApply(opts.adminUser);
|
|
19
|
+
if (res === false) {
|
|
20
|
+
return { ok: true };
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (ctx.options.usersFilterToAllowSkipSetup) {
|
|
24
|
+
const res = yield ctx.checkIfSkipSetupAllowSkipVerify(opts.adminUser);
|
|
25
|
+
if (res.skipAllowed === true) {
|
|
26
|
+
return { ok: true };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (ctx.options.stepUpMfaGracePeriodSeconds && ((_c = opts.extra) === null || _c === void 0 ? void 0 : _c.headers) && !confirmationResult.mode) {
|
|
30
|
+
const verificationResult = yield ctx.cookieService.isMfaGraceValid(opts.extra.headers, cookies);
|
|
31
|
+
if (verificationResult === true) {
|
|
32
|
+
return { ok: true };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const mode = confirmationResult.mode;
|
|
36
|
+
if (!mode) {
|
|
37
|
+
return { error: "Unsupported confirmation mode" };
|
|
38
|
+
}
|
|
39
|
+
if (mode === "totp") {
|
|
40
|
+
const { result: code } = confirmationResult;
|
|
41
|
+
const verificationResult = yield ctx.totpService.verifyUserCode(opts.userPk, code);
|
|
42
|
+
if ('ok' in verificationResult && verificationResult.ok) {
|
|
43
|
+
if (ctx.options.stepUpMfaGracePeriodSeconds) {
|
|
44
|
+
ctx.cookieService.issueMfaGrace({ headers: (_d = opts.extra) === null || _d === void 0 ? void 0 : _d.headers, response }, cookies);
|
|
45
|
+
}
|
|
46
|
+
return { ok: true };
|
|
47
|
+
}
|
|
48
|
+
return verificationResult;
|
|
49
|
+
}
|
|
50
|
+
else if (mode === "passkey") {
|
|
51
|
+
const { result } = confirmationResult;
|
|
52
|
+
const cookiesValidationResult = yield ctx.passkeyService.validateLoginChallengeCookie(cookies);
|
|
53
|
+
if (!cookiesValidationResult.ok) {
|
|
54
|
+
return { error: cookiesValidationResult.error };
|
|
55
|
+
}
|
|
56
|
+
const verificationResult = yield ctx.passkeyService.verifyPasskeyResponse(result, opts.userPk, cookiesValidationResult.decodedPasskeysCookies);
|
|
57
|
+
if (verificationResult.ok && verificationResult.passkeyConfirmed) {
|
|
58
|
+
if (ctx.options.stepUpMfaGracePeriodSeconds) {
|
|
59
|
+
ctx.cookieService.issueMfaGrace({ headers: (_e = opts.extra) === null || _e === void 0 ? void 0 : _e.headers, response }, cookies);
|
|
60
|
+
}
|
|
61
|
+
return { ok: true };
|
|
62
|
+
}
|
|
63
|
+
return { error: "Invalid passkey" };
|
|
64
|
+
}
|
|
65
|
+
return { error: "Unsupported confirmation mode" };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ErrorResponse } from "./types.js";
|
|
2
|
+
export declare function errorMessage(error: unknown): string;
|
|
3
|
+
export declare function errorResult(message: string): ErrorResponse;
|
|
4
|
+
export declare function prefixedErrorResult(prefix: string, error: unknown): ErrorResponse;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function errorMessage(error) {
|
|
2
|
+
return error instanceof Error ? error.message : String(error);
|
|
3
|
+
}
|
|
4
|
+
export function errorResult(message) {
|
|
5
|
+
return { ok: false, error: message };
|
|
6
|
+
}
|
|
7
|
+
export function prefixedErrorResult(prefix, error) {
|
|
8
|
+
return errorResult(`${prefix}${errorMessage(error)}`);
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parsePeriod(period?: string): number;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const PERIOD_RE = /^(\d+)([dhms])$/;
|
|
2
|
+
export function parsePeriod(period) {
|
|
3
|
+
if (!period)
|
|
4
|
+
return 0;
|
|
5
|
+
const match = PERIOD_RE.exec(period.trim());
|
|
6
|
+
if (!match)
|
|
7
|
+
throw new Error(`Invalid suggestionPeriod format: ${period}`);
|
|
8
|
+
const value = parseInt(match[1], 10);
|
|
9
|
+
const unit = match[2];
|
|
10
|
+
switch (unit) {
|
|
11
|
+
case 'd': return value * 24 * 60 * 60 * 1000;
|
|
12
|
+
case 'h': return value * 60 * 60 * 1000;
|
|
13
|
+
case 'm': return value * 60 * 1000;
|
|
14
|
+
case 's': return value * 1000;
|
|
15
|
+
default: return value;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HttpExtra } from "adminforth";
|
|
2
|
+
export type AnyRecord = Record<string, any>;
|
|
3
|
+
export type CookieList = HttpExtra["cookies"];
|
|
4
|
+
export type HttpHeaders = HttpExtra["headers"];
|
|
5
|
+
export type ErrorResponse = {
|
|
6
|
+
ok: false;
|
|
7
|
+
code?: string;
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
export type OkResponse<T extends AnyRecord = AnyRecord> = {
|
|
11
|
+
ok: true;
|
|
12
|
+
} & T;
|
|
13
|
+
export type PluginResponse<T extends AnyRecord = AnyRecord> = OkResponse<T> | ErrorResponse;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IHttpServer } from "adminforth";
|
|
2
|
+
|
|
3
|
+
export function registerPasskeyEndpoints(server: IHttpServer, handlers: any): void {
|
|
4
|
+
server.endpoint({
|
|
5
|
+
method: 'POST',
|
|
6
|
+
path: `/plugin/passkeys/registrationOptions`,
|
|
7
|
+
noAuth: false,
|
|
8
|
+
handler: async ({ body, adminUser, response, cookies, headers }) =>
|
|
9
|
+
handlers.createRegistrationOptions({ body, adminUser, response, cookies, headers }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
server.endpoint({
|
|
13
|
+
method: 'POST',
|
|
14
|
+
path: `/plugin/passkeys/finishRegistration`,
|
|
15
|
+
noAuth: false,
|
|
16
|
+
handler: async ({ body, adminUser, cookies }) => handlers.finishRegistration({ body, adminUser, cookies }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
server.endpoint({
|
|
20
|
+
method: 'POST',
|
|
21
|
+
path: `/plugin/passkeys/loginOptions`,
|
|
22
|
+
noAuth: true,
|
|
23
|
+
handler: async ({ response }) => handlers.createLoginOptions({ response }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
server.endpoint({
|
|
27
|
+
method: 'GET',
|
|
28
|
+
path: `/plugin/passkeys/getPasskeys`,
|
|
29
|
+
noAuth: false,
|
|
30
|
+
handler: async ({ adminUser }) => handlers.getPasskeys({ adminUser }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
server.endpoint({
|
|
34
|
+
method: 'DELETE',
|
|
35
|
+
path: `/plugin/passkeys/deletePasskey`,
|
|
36
|
+
noAuth: false,
|
|
37
|
+
handler: async ({ body, adminUser }) => handlers.deletePasskey({ body, adminUser }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
server.endpoint({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
path: `/plugin/passkeys/renamePasskey`,
|
|
43
|
+
noAuth: false,
|
|
44
|
+
handler: async ({ body, adminUser }) => handlers.renamePasskey({ body, adminUser }),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
server.endpoint({
|
|
48
|
+
method: 'POST',
|
|
49
|
+
path: `/plugin/passkeys/checkIfUserHasPasskeys`,
|
|
50
|
+
noAuth: true,
|
|
51
|
+
handler: async ({ cookies }) => handlers.checkIfUserHasPasskeys({ cookies }),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
server.endpoint({
|
|
55
|
+
method: 'POST',
|
|
56
|
+
path: `/plugin/passkeys/resolveVerifyAuto`,
|
|
57
|
+
noAuth: false,
|
|
58
|
+
handler: async ({ body, adminUser, response, cookies, headers }) =>
|
|
59
|
+
handlers.resolveVerifyAuto({ body, adminUser, response, cookies, headers }),
|
|
60
|
+
});
|
|
61
|
+
}
|