@absolutejs/auth 0.75.3 → 0.76.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/dist/cli/migrate.js +13 -2
- package/dist/cli/migrate.js.map +4 -4
- package/dist/index.d.ts +80 -4
- package/dist/index.js +290 -57
- package/dist/index.js.map +12 -12
- package/dist/mfa/challenge.d.ts +53 -7
- package/dist/mfa/management.d.ts +49 -6
- package/dist/mfa/postgresMfaStore.d.ts +31 -1
- package/dist/mfa/rotation.d.ts +1 -1
- package/dist/mfa/routes.d.ts +78 -2
- package/dist/mfa/sms.d.ts +7 -2
- package/dist/mfa/totp.d.ts +8 -2
- package/dist/mfa/types.d.ts +47 -0
- package/dist/server.js +288 -57
- package/dist/server.js.map +12 -12
- package/package.json +1 -1
- package/dist/client/dpop.test.d.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -6185,7 +6185,45 @@ var createLockoutGuard = ({
|
|
|
6185
6185
|
});
|
|
6186
6186
|
|
|
6187
6187
|
// src/mfa/types.ts
|
|
6188
|
-
var
|
|
6188
|
+
var LEGACY_SMS_FACTOR_ID = "legacy-sms";
|
|
6189
|
+
var LEGACY_TOTP_FACTOR_ID = "legacy-totp";
|
|
6190
|
+
var getMfaFactors = (enrollment) => {
|
|
6191
|
+
if (enrollment.factors !== undefined)
|
|
6192
|
+
return enrollment.factors;
|
|
6193
|
+
const factors = [];
|
|
6194
|
+
if (enrollment.totpSecretCiphertext) {
|
|
6195
|
+
factors.push({
|
|
6196
|
+
id: LEGACY_TOTP_FACTOR_ID,
|
|
6197
|
+
label: "Authenticator app",
|
|
6198
|
+
secretCiphertext: enrollment.totpSecretCiphertext,
|
|
6199
|
+
type: "totp",
|
|
6200
|
+
verified: enrollment.totpVerified
|
|
6201
|
+
});
|
|
6202
|
+
}
|
|
6203
|
+
if (enrollment.smsPhone) {
|
|
6204
|
+
factors.push({
|
|
6205
|
+
id: LEGACY_SMS_FACTOR_ID,
|
|
6206
|
+
label: "Text message",
|
|
6207
|
+
phone: enrollment.smsPhone,
|
|
6208
|
+
type: "sms",
|
|
6209
|
+
verified: enrollment.smsVerified
|
|
6210
|
+
});
|
|
6211
|
+
}
|
|
6212
|
+
return factors;
|
|
6213
|
+
};
|
|
6214
|
+
var isMfaEnrolled = (enrollment) => enrollment !== undefined && (getMfaFactors(enrollment).some((factor) => factor.verified) || enrollment.backupCodeHashes.length > 0);
|
|
6215
|
+
var withMfaFactors = (enrollment, factors) => {
|
|
6216
|
+
const firstSms = factors.find((factor) => factor.type === "sms");
|
|
6217
|
+
const firstTotp = factors.find((factor) => factor.type === "totp");
|
|
6218
|
+
return {
|
|
6219
|
+
...enrollment,
|
|
6220
|
+
factors,
|
|
6221
|
+
smsPhone: firstSms?.phone,
|
|
6222
|
+
smsVerified: firstSms?.verified ?? false,
|
|
6223
|
+
totpSecretCiphertext: firstTotp?.secretCiphertext,
|
|
6224
|
+
totpVerified: firstTotp?.verified ?? false
|
|
6225
|
+
};
|
|
6226
|
+
};
|
|
6189
6227
|
|
|
6190
6228
|
// src/mfa/gate.ts
|
|
6191
6229
|
var createMfaGate = ({ getUserId, mfaStore }) => async (user) => isMfaEnrolled(await mfaStore.getEnrollment(getUserId(user)));
|
|
@@ -6401,6 +6439,8 @@ var hasRecentAuthentication = (session, maxAgeMs, now = Date.now()) => session.a
|
|
|
6401
6439
|
// src/mfa/sms.ts
|
|
6402
6440
|
var DECIMAL_RADIX2 = 10;
|
|
6403
6441
|
var MASK_VISIBLE_DIGITS = 4;
|
|
6442
|
+
var FACTOR_LABEL_MAX_LENGTH = 80;
|
|
6443
|
+
var DEFAULT_SMS_LABEL = "Text message";
|
|
6404
6444
|
var E164_PATTERN = /^\+[1-9]\d{7,14}$/u;
|
|
6405
6445
|
var generateNumericCode = (length) => {
|
|
6406
6446
|
const bytes = new Uint8Array(length);
|
|
@@ -6574,10 +6614,13 @@ var mfaSmsRoutes = ({
|
|
|
6574
6614
|
smsSetupRoute = "/auth/mfa/sms/setup",
|
|
6575
6615
|
smsVerifyRoute = "/auth/mfa/sms/verify"
|
|
6576
6616
|
}) => new Elysia18().use(sessionStore()).post(smsSetupRoute, {
|
|
6577
|
-
body: t12.Object({
|
|
6617
|
+
body: t12.Object({
|
|
6618
|
+
label: t12.Optional(t12.String({ maxLength: FACTOR_LABEL_MAX_LENGTH })),
|
|
6619
|
+
phone: t12.String()
|
|
6620
|
+
}),
|
|
6578
6621
|
cookie: t12.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6579
6622
|
}, async ({
|
|
6580
|
-
body: { phone },
|
|
6623
|
+
body: { label, phone },
|
|
6581
6624
|
cookie: { user_session_id },
|
|
6582
6625
|
status,
|
|
6583
6626
|
store: { session }
|
|
@@ -6605,21 +6648,35 @@ var mfaSmsRoutes = ({
|
|
|
6605
6648
|
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
6606
6649
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6607
6650
|
}
|
|
6651
|
+
const base = existing ?? {
|
|
6652
|
+
backupCodeHashes: [],
|
|
6653
|
+
createdAt: now,
|
|
6654
|
+
smsVerified: false,
|
|
6655
|
+
totpVerified: false,
|
|
6656
|
+
updatedAt: now,
|
|
6657
|
+
userId
|
|
6658
|
+
};
|
|
6659
|
+
const factor = {
|
|
6660
|
+
id: crypto.randomUUID(),
|
|
6661
|
+
label: label?.trim() || DEFAULT_SMS_LABEL,
|
|
6662
|
+
phone,
|
|
6663
|
+
type: "sms",
|
|
6664
|
+
verified: false
|
|
6665
|
+
};
|
|
6666
|
+
const factors = [
|
|
6667
|
+
...getMfaFactors(base).filter((existingFactor) => existingFactor.type !== "sms" || existingFactor.verified),
|
|
6668
|
+
factor
|
|
6669
|
+
];
|
|
6608
6670
|
try {
|
|
6609
6671
|
await issueAndStoreSmsCode({
|
|
6610
6672
|
codeLength: smsCodeLength,
|
|
6611
6673
|
enrollment: {
|
|
6612
|
-
|
|
6613
|
-
createdAt: existing?.createdAt ?? now,
|
|
6614
|
-
lastUsedAt: existing?.lastUsedAt,
|
|
6674
|
+
...withMfaFactors(base, factors),
|
|
6615
6675
|
smsFailedAttempts: 0,
|
|
6676
|
+
smsPendingFactorId: factor.id,
|
|
6616
6677
|
smsPhone: phone,
|
|
6617
|
-
smsVerified: false,
|
|
6618
|
-
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
6619
|
-
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
6620
|
-
totpVerified: existing?.totpVerified ?? false,
|
|
6621
6678
|
updatedAt: now,
|
|
6622
|
-
userId
|
|
6679
|
+
userId: base.userId
|
|
6623
6680
|
},
|
|
6624
6681
|
mfaStore,
|
|
6625
6682
|
onSendSmsCode,
|
|
@@ -6636,12 +6693,18 @@ var mfaSmsRoutes = ({
|
|
|
6636
6693
|
throw error;
|
|
6637
6694
|
return status(mapped.status, mapped.message);
|
|
6638
6695
|
}
|
|
6639
|
-
return status("OK", {
|
|
6696
|
+
return status("OK", {
|
|
6697
|
+
factorId: factor.id,
|
|
6698
|
+
phone: maskPhone(phone)
|
|
6699
|
+
});
|
|
6640
6700
|
}).post(smsVerifyRoute, {
|
|
6641
|
-
body: t12.Object({
|
|
6701
|
+
body: t12.Object({
|
|
6702
|
+
code: t12.String(),
|
|
6703
|
+
factorId: t12.Optional(t12.String())
|
|
6704
|
+
}),
|
|
6642
6705
|
cookie: t12.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6643
6706
|
}, async ({
|
|
6644
|
-
body: { code },
|
|
6707
|
+
body: { code, factorId },
|
|
6645
6708
|
cookie: { user_session_id },
|
|
6646
6709
|
status,
|
|
6647
6710
|
store: { session }
|
|
@@ -6662,6 +6725,12 @@ var mfaSmsRoutes = ({
|
|
|
6662
6725
|
if (!enrollment?.smsPhone) {
|
|
6663
6726
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
6664
6727
|
}
|
|
6728
|
+
const factors = getMfaFactors(enrollment);
|
|
6729
|
+
const pendingFactorId = factorId ?? enrollment.smsPendingFactorId;
|
|
6730
|
+
const pendingFactor = factors.find((candidate) => candidate.type === "sms" && (candidate.id === pendingFactorId || pendingFactorId === undefined && candidate.phone === enrollment.smsPhone));
|
|
6731
|
+
if (!pendingFactor || pendingFactor.type !== "sms") {
|
|
6732
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
6733
|
+
}
|
|
6665
6734
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6666
6735
|
const challengeId = enrollment.smsChallengeId;
|
|
6667
6736
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
@@ -6689,7 +6758,7 @@ var mfaSmsRoutes = ({
|
|
|
6689
6758
|
purpose: "mfa_enrollment",
|
|
6690
6759
|
reference: providerReference,
|
|
6691
6760
|
subject: userId,
|
|
6692
|
-
to:
|
|
6761
|
+
to: pendingFactor.phone
|
|
6693
6762
|
});
|
|
6694
6763
|
if (checked.error !== undefined) {
|
|
6695
6764
|
return status(checked.error.status, checked.error.message);
|
|
@@ -6705,8 +6774,10 @@ var mfaSmsRoutes = ({
|
|
|
6705
6774
|
});
|
|
6706
6775
|
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
|
|
6707
6776
|
}
|
|
6777
|
+
const verifiedFactors = factors.map((candidate) => candidate.id === pendingFactor.id ? { ...candidate, verified: true } : candidate);
|
|
6708
6778
|
const completed = await mfaStore.completeSmsChallenge({
|
|
6709
6779
|
challengeId,
|
|
6780
|
+
factors: verifiedFactors,
|
|
6710
6781
|
smsVerified: true,
|
|
6711
6782
|
userId
|
|
6712
6783
|
});
|
|
@@ -6736,15 +6807,50 @@ var mfaChallenge = ({
|
|
|
6736
6807
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
6737
6808
|
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
6738
6809
|
totpMaxAttempts = DEFAULT_TOTP_MAX_ATTEMPTS
|
|
6739
|
-
}) => new Elysia19().use(sessionStore()).
|
|
6810
|
+
}) => new Elysia19().use(sessionStore()).get(challengeRoute, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) }, async ({
|
|
6811
|
+
cookie: { user_session_id },
|
|
6812
|
+
status,
|
|
6813
|
+
store: { unregisteredSession }
|
|
6814
|
+
}) => {
|
|
6815
|
+
const compatibilityLayer = await createSessionCompatibilityLayer({
|
|
6816
|
+
authSessionStore,
|
|
6817
|
+
userSessionId: user_session_id.value
|
|
6818
|
+
});
|
|
6819
|
+
const challengeUnregistered = authSessionStore ? compatibilityLayer.unregisteredSession : unregisteredSession;
|
|
6820
|
+
const pendingId = user_session_id.value;
|
|
6821
|
+
const pending = pendingId ? challengeUnregistered[pendingId] : undefined;
|
|
6822
|
+
if (!pending) {
|
|
6823
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
6824
|
+
}
|
|
6825
|
+
const user = await getChallengeUser(pending.userIdentity ?? {});
|
|
6826
|
+
const enrollment = user ? await mfaStore.getEnrollment(getUserId(user)) : undefined;
|
|
6827
|
+
if (!enrollment) {
|
|
6828
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
6829
|
+
}
|
|
6830
|
+
const response = {
|
|
6831
|
+
backupCodesAvailable: enrollment.backupCodeHashes.length > 0,
|
|
6832
|
+
factors: getMfaFactors(enrollment).filter((factor) => factor.verified).map((factor) => ({
|
|
6833
|
+
id: factor.id,
|
|
6834
|
+
label: factor.label,
|
|
6835
|
+
phone: factor.type === "sms" ? maskPhone(factor.phone) : null,
|
|
6836
|
+
type: factor.type
|
|
6837
|
+
}))
|
|
6838
|
+
};
|
|
6839
|
+
return status("OK", response);
|
|
6840
|
+
}).post(challengeRoute, {
|
|
6740
6841
|
body: t13.Object({
|
|
6741
6842
|
action: t13.Optional(t13.Union([t13.Literal("send"), t13.Literal("verify")])),
|
|
6742
6843
|
code: t13.Optional(t13.String()),
|
|
6743
|
-
factor: t13.Optional(t13.
|
|
6844
|
+
factor: t13.Optional(t13.Union([
|
|
6845
|
+
t13.Literal("backup_codes"),
|
|
6846
|
+
t13.Literal("sms"),
|
|
6847
|
+
t13.Literal("totp")
|
|
6848
|
+
])),
|
|
6849
|
+
factorId: t13.Optional(t13.String())
|
|
6744
6850
|
}),
|
|
6745
6851
|
cookie: t13.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6746
6852
|
}, async ({
|
|
6747
|
-
body: { action, code, factor },
|
|
6853
|
+
body: { action, code, factor, factorId },
|
|
6748
6854
|
cookie: { user_session_id },
|
|
6749
6855
|
status,
|
|
6750
6856
|
store: { session, unregisteredSession }
|
|
@@ -6765,6 +6871,7 @@ var mfaChallenge = ({
|
|
|
6765
6871
|
if (!user || !enrollment) {
|
|
6766
6872
|
return status("Unauthorized", "No MFA challenge in progress");
|
|
6767
6873
|
}
|
|
6874
|
+
const factors = getMfaFactors(enrollment);
|
|
6768
6875
|
const promote = async () => {
|
|
6769
6876
|
delete challengeUnregistered[pendingId];
|
|
6770
6877
|
const userSessionId = crypto.randomUUID();
|
|
@@ -6787,10 +6894,18 @@ var mfaChallenge = ({
|
|
|
6787
6894
|
if (enrollment.smsCodeSentAt !== undefined && Date.now() - enrollment.smsCodeSentAt < smsResendCooldownMs) {
|
|
6788
6895
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6789
6896
|
}
|
|
6897
|
+
const smsFactor = factors.find((candidate) => candidate.type === "sms" && candidate.verified && (candidate.id === factorId || factorId === undefined));
|
|
6898
|
+
if (!smsFactor || smsFactor.type !== "sms") {
|
|
6899
|
+
return status("Bad Request", "SMS factor not found");
|
|
6900
|
+
}
|
|
6790
6901
|
try {
|
|
6791
6902
|
await issueAndStoreSmsCode({
|
|
6792
6903
|
codeLength: smsCodeLength,
|
|
6793
|
-
enrollment
|
|
6904
|
+
enrollment: {
|
|
6905
|
+
...withMfaFactors(enrollment, factors),
|
|
6906
|
+
smsPendingFactorId: smsFactor.id,
|
|
6907
|
+
smsPhone: smsFactor.phone
|
|
6908
|
+
},
|
|
6794
6909
|
mfaStore,
|
|
6795
6910
|
onSendSmsCode,
|
|
6796
6911
|
previousEnrollment: enrollment,
|
|
@@ -6806,10 +6921,19 @@ var mfaChallenge = ({
|
|
|
6806
6921
|
throw error;
|
|
6807
6922
|
return status(mapped.status, mapped.message);
|
|
6808
6923
|
}
|
|
6809
|
-
return status("OK", {
|
|
6924
|
+
return status("OK", {
|
|
6925
|
+
factorId: smsFactor.id,
|
|
6926
|
+
phone: maskPhone(smsFactor.phone),
|
|
6927
|
+
status: "sent"
|
|
6928
|
+
});
|
|
6810
6929
|
};
|
|
6811
6930
|
const runSmsChallenge = async () => {
|
|
6812
|
-
if (
|
|
6931
|
+
if (action !== "send" && factorId !== undefined && enrollment.smsPendingFactorId !== undefined && factorId !== enrollment.smsPendingFactorId) {
|
|
6932
|
+
return status("Bad Request", "No SMS code in progress");
|
|
6933
|
+
}
|
|
6934
|
+
const selectedFactorId = enrollment.smsPendingFactorId ?? factorId;
|
|
6935
|
+
const smsFactor = factors.find((candidate) => candidate.type === "sms" && candidate.verified && (candidate.id === selectedFactorId || selectedFactorId === undefined));
|
|
6936
|
+
if (!smsFactor || smsFactor.type !== "sms") {
|
|
6813
6937
|
return status("Unauthorized", "No MFA challenge in progress");
|
|
6814
6938
|
}
|
|
6815
6939
|
if (action === "send")
|
|
@@ -6848,7 +6972,7 @@ var mfaChallenge = ({
|
|
|
6848
6972
|
purpose: "mfa_challenge",
|
|
6849
6973
|
reference: providerReference,
|
|
6850
6974
|
subject: getUserId(user),
|
|
6851
|
-
to:
|
|
6975
|
+
to: smsFactor.phone
|
|
6852
6976
|
});
|
|
6853
6977
|
if (checked.error !== undefined) {
|
|
6854
6978
|
return status(checked.error.status, checked.error.message);
|
|
@@ -6870,6 +6994,7 @@ var mfaChallenge = ({
|
|
|
6870
6994
|
}
|
|
6871
6995
|
const completed = await mfaStore.completeSmsChallenge({
|
|
6872
6996
|
challengeId,
|
|
6997
|
+
factors,
|
|
6873
6998
|
lastUsedAt: Date.now(),
|
|
6874
6999
|
smsVerified: true,
|
|
6875
7000
|
userId: getUserId(user)
|
|
@@ -6891,11 +7016,14 @@ var mfaChallenge = ({
|
|
|
6891
7016
|
});
|
|
6892
7017
|
return status("Unauthorized", "Too many attempts");
|
|
6893
7018
|
}
|
|
6894
|
-
const
|
|
6895
|
-
|
|
7019
|
+
const totpFactors = factors.filter((candidate) => candidate.type === "totp" && candidate.verified && (candidate.id === factorId || factorId === undefined));
|
|
7020
|
+
const totpChecks = factor === "backup_codes" ? [] : await Promise.all(totpFactors.map(async (candidate) => verifyTotp({
|
|
7021
|
+
secret: await decryptTotpSecret(candidate.secretCiphertext, encryptionKey),
|
|
6896
7022
|
token: code
|
|
6897
|
-
})
|
|
6898
|
-
const
|
|
7023
|
+
})));
|
|
7024
|
+
const totpValid = totpChecks.some(Boolean);
|
|
7025
|
+
const mayUseBackupCode = factor === "backup_codes" || factor === undefined && factorId === undefined;
|
|
7026
|
+
const remainingBackupHashes = totpValid || !mayUseBackupCode ? undefined : await consumeBackupCode(code, enrollment.backupCodeHashes);
|
|
6899
7027
|
if (!totpValid && remainingBackupHashes === undefined) {
|
|
6900
7028
|
await mfaStore.saveEnrollment({
|
|
6901
7029
|
...enrollment,
|
|
@@ -6927,6 +7055,12 @@ var maskPhone2 = (phone) => {
|
|
|
6927
7055
|
const suffix = phone.slice(-visibleDigits);
|
|
6928
7056
|
return `${"*".repeat(Math.max(0, phone.length - visibleDigits))}${suffix}`;
|
|
6929
7057
|
};
|
|
7058
|
+
var publicFactors = (enrollment) => enrollment ? getMfaFactors(enrollment).filter((factor) => factor.verified).map((factor) => ({
|
|
7059
|
+
id: factor.id,
|
|
7060
|
+
label: factor.label,
|
|
7061
|
+
phone: factor.type === "sms" ? maskPhone2(factor.phone) : null,
|
|
7062
|
+
type: factor.type
|
|
7063
|
+
})) : [];
|
|
6930
7064
|
var mfaManagementRoutes = ({
|
|
6931
7065
|
authSessionStore,
|
|
6932
7066
|
getUserId,
|
|
@@ -6950,6 +7084,7 @@ var mfaManagementRoutes = ({
|
|
|
6950
7084
|
const response = {
|
|
6951
7085
|
backupCodesRemaining: enrollment?.backupCodeHashes.length ?? 0,
|
|
6952
7086
|
enabled: isMfaEnrolled(enrollment),
|
|
7087
|
+
factors: publicFactors(enrollment),
|
|
6953
7088
|
smsBackup: {
|
|
6954
7089
|
enabled: enrollment?.smsVerified ?? false,
|
|
6955
7090
|
phone: maskPhone2(enrollment?.smsPhone)
|
|
@@ -6975,11 +7110,54 @@ var mfaManagementRoutes = ({
|
|
|
6975
7110
|
}
|
|
6976
7111
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
6977
7112
|
return status("OK", { status: "disabled" });
|
|
7113
|
+
}).delete(`${managementRoute}/factors/:factorId`, {
|
|
7114
|
+
cookie: t14.Cookie({ user_session_id: userSessionIdTypebox }),
|
|
7115
|
+
params: t14.Object({ factorId: t14.String() })
|
|
7116
|
+
}, async ({
|
|
7117
|
+
cookie: { user_session_id },
|
|
7118
|
+
params: { factorId },
|
|
7119
|
+
status,
|
|
7120
|
+
store: { session }
|
|
7121
|
+
}) => {
|
|
7122
|
+
const userSession = await loadSessionFromSource({
|
|
7123
|
+
authSessionStore,
|
|
7124
|
+
session,
|
|
7125
|
+
userSessionId: user_session_id.value
|
|
7126
|
+
});
|
|
7127
|
+
if (!userSession) {
|
|
7128
|
+
return status("Unauthorized", "Authentication required");
|
|
7129
|
+
}
|
|
7130
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7131
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7132
|
+
}
|
|
7133
|
+
const userId = getUserId(userSession.user);
|
|
7134
|
+
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7135
|
+
if (!enrollment) {
|
|
7136
|
+
return status("Not Found", "MFA factor not found");
|
|
7137
|
+
}
|
|
7138
|
+
const factors = getMfaFactors(enrollment);
|
|
7139
|
+
if (!factors.some((factor) => factor.id === factorId)) {
|
|
7140
|
+
return status("Not Found", "MFA factor not found");
|
|
7141
|
+
}
|
|
7142
|
+
const remaining = factors.filter((factor) => factor.id !== factorId);
|
|
7143
|
+
if (remaining.length === 0) {
|
|
7144
|
+
await mfaStore.removeEnrollment(userId);
|
|
7145
|
+
} else {
|
|
7146
|
+
const hasVerifiedFactor = remaining.some((factor) => factor.verified);
|
|
7147
|
+
await mfaStore.saveEnrollment(withMfaFactors({
|
|
7148
|
+
...enrollment,
|
|
7149
|
+
backupCodeHashes: hasVerifiedFactor ? enrollment.backupCodeHashes : [],
|
|
7150
|
+
updatedAt: Date.now()
|
|
7151
|
+
}, remaining));
|
|
7152
|
+
}
|
|
7153
|
+
return status("OK", { status: "removed" });
|
|
6978
7154
|
});
|
|
6979
7155
|
|
|
6980
7156
|
// src/mfa/totp.ts
|
|
6981
7157
|
init_crypto();
|
|
6982
7158
|
import { Elysia as Elysia21, t as t15 } from "elysia";
|
|
7159
|
+
var FACTOR_LABEL_MAX_LENGTH2 = 80;
|
|
7160
|
+
var DEFAULT_TOTP_LABEL = "Authenticator app";
|
|
6983
7161
|
var mfaTotpRoutes = ({
|
|
6984
7162
|
authSessionStore,
|
|
6985
7163
|
backupCodeCount = DEFAULT_BACKUP_CODE_COUNT,
|
|
@@ -6991,7 +7169,13 @@ var mfaTotpRoutes = ({
|
|
|
6991
7169
|
onMfaEnrolled,
|
|
6992
7170
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
6993
7171
|
totpVerifyRoute = "/auth/mfa/totp/verify"
|
|
6994
|
-
}) => new Elysia21().use(sessionStore()).post(totpSetupRoute, {
|
|
7172
|
+
}) => new Elysia21().use(sessionStore()).post(totpSetupRoute, {
|
|
7173
|
+
body: t15.Object({
|
|
7174
|
+
label: t15.Optional(t15.String({ maxLength: FACTOR_LABEL_MAX_LENGTH2 }))
|
|
7175
|
+
}),
|
|
7176
|
+
cookie: t15.Cookie({ user_session_id: userSessionIdTypebox })
|
|
7177
|
+
}, async ({
|
|
7178
|
+
body: { label },
|
|
6995
7179
|
cookie: { user_session_id },
|
|
6996
7180
|
status,
|
|
6997
7181
|
store: { session }
|
|
@@ -7011,20 +7195,25 @@ var mfaTotpRoutes = ({
|
|
|
7011
7195
|
const secret = generateTotpSecret();
|
|
7012
7196
|
const existing = await mfaStore.getEnrollment(userId);
|
|
7013
7197
|
const now = Date.now();
|
|
7014
|
-
|
|
7015
|
-
backupCodeHashes:
|
|
7016
|
-
createdAt:
|
|
7017
|
-
|
|
7018
|
-
smsPendingCodeExpiresAt: existing?.smsPendingCodeExpiresAt,
|
|
7019
|
-
smsPendingCodeHash: existing?.smsPendingCodeHash,
|
|
7020
|
-
smsPhone: existing?.smsPhone,
|
|
7021
|
-
smsVerified: existing?.smsVerified ?? false,
|
|
7022
|
-
totpSecretCiphertext: await encryptTotpSecret(secret, encryptionKey),
|
|
7198
|
+
const base = existing ?? {
|
|
7199
|
+
backupCodeHashes: [],
|
|
7200
|
+
createdAt: now,
|
|
7201
|
+
smsVerified: false,
|
|
7023
7202
|
totpVerified: false,
|
|
7024
7203
|
updatedAt: now,
|
|
7025
7204
|
userId
|
|
7026
|
-
}
|
|
7205
|
+
};
|
|
7206
|
+
const factor = {
|
|
7207
|
+
id: crypto.randomUUID(),
|
|
7208
|
+
label: label?.trim() || DEFAULT_TOTP_LABEL,
|
|
7209
|
+
secretCiphertext: await encryptTotpSecret(secret, encryptionKey),
|
|
7210
|
+
type: "totp",
|
|
7211
|
+
verified: false
|
|
7212
|
+
};
|
|
7213
|
+
const factors = getMfaFactors(base).filter((existingFactor) => existingFactor.type !== "totp" || existingFactor.verified);
|
|
7214
|
+
await mfaStore.saveEnrollment(withMfaFactors({ ...base, updatedAt: now }, [...factors, factor]));
|
|
7027
7215
|
return status("OK", {
|
|
7216
|
+
factorId: factor.id,
|
|
7028
7217
|
secret,
|
|
7029
7218
|
uri: createTotpKeyUri({
|
|
7030
7219
|
accountName: userId,
|
|
@@ -7033,10 +7222,13 @@ var mfaTotpRoutes = ({
|
|
|
7033
7222
|
})
|
|
7034
7223
|
});
|
|
7035
7224
|
}).post(totpVerifyRoute, {
|
|
7036
|
-
body: t15.Object({
|
|
7225
|
+
body: t15.Object({
|
|
7226
|
+
code: t15.String(),
|
|
7227
|
+
factorId: t15.Optional(t15.String())
|
|
7228
|
+
}),
|
|
7037
7229
|
cookie: t15.Cookie({ user_session_id: userSessionIdTypebox })
|
|
7038
7230
|
}, async ({
|
|
7039
|
-
body: { code },
|
|
7231
|
+
body: { code, factorId },
|
|
7040
7232
|
cookie: { user_session_id },
|
|
7041
7233
|
status,
|
|
7042
7234
|
store: { session }
|
|
@@ -7054,21 +7246,26 @@ var mfaTotpRoutes = ({
|
|
|
7054
7246
|
}
|
|
7055
7247
|
const userId = getUserId(userSession.user);
|
|
7056
7248
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7057
|
-
if (!enrollment
|
|
7249
|
+
if (!enrollment) {
|
|
7250
|
+
return status("Bad Request", "No TOTP enrollment in progress");
|
|
7251
|
+
}
|
|
7252
|
+
const factors = getMfaFactors(enrollment);
|
|
7253
|
+
const factor = factors.find((candidate) => candidate.type === "totp" && (candidate.id === factorId || factorId === undefined && !candidate.verified));
|
|
7254
|
+
if (!factor || factor.type !== "totp") {
|
|
7058
7255
|
return status("Bad Request", "No TOTP enrollment in progress");
|
|
7059
7256
|
}
|
|
7060
|
-
const secret = await decryptTotpSecret(
|
|
7257
|
+
const secret = await decryptTotpSecret(factor.secretCiphertext, encryptionKey);
|
|
7061
7258
|
const valid = await verifyTotp({ secret, token: code });
|
|
7062
7259
|
if (!valid) {
|
|
7063
7260
|
return status("Bad Request", "Invalid TOTP code");
|
|
7064
7261
|
}
|
|
7065
7262
|
const { codes, hashes } = await generateBackupCodes(backupCodeCount);
|
|
7066
|
-
|
|
7263
|
+
const verifiedFactors = factors.map((candidate) => candidate.id === factor.id ? { ...candidate, verified: true } : candidate);
|
|
7264
|
+
await mfaStore.saveEnrollment(withMfaFactors({
|
|
7067
7265
|
...enrollment,
|
|
7068
7266
|
backupCodeHashes: hashes,
|
|
7069
|
-
totpVerified: true,
|
|
7070
7267
|
updatedAt: Date.now()
|
|
7071
|
-
});
|
|
7268
|
+
}, verifiedFactors));
|
|
7072
7269
|
await onMfaEnrolled?.({ userId });
|
|
7073
7270
|
return status("OK", { backupCodes: codes });
|
|
7074
7271
|
});
|
|
@@ -34782,23 +34979,33 @@ var ensureReadable = async (ciphertext, key, userId) => {
|
|
|
34782
34979
|
}
|
|
34783
34980
|
};
|
|
34784
34981
|
var planEnrollment = async (enrollment, oldKey, newKey) => {
|
|
34785
|
-
const
|
|
34786
|
-
|
|
34982
|
+
const factors = getMfaFactors(enrollment);
|
|
34983
|
+
const totpFactors = factors.filter((factor) => factor.type === "totp");
|
|
34984
|
+
if (totpFactors.length === 0) {
|
|
34787
34985
|
const plan2 = { kind: "skip" };
|
|
34788
34986
|
return plan2;
|
|
34789
34987
|
}
|
|
34790
|
-
|
|
34791
|
-
|
|
34792
|
-
|
|
34988
|
+
let rotated = false;
|
|
34989
|
+
const nextFactors = await Promise.all(factors.map(async (factor) => {
|
|
34990
|
+
if (factor.type !== "totp")
|
|
34991
|
+
return factor;
|
|
34992
|
+
const secret = await tryDecrypt(factor.secretCiphertext, oldKey);
|
|
34993
|
+
if (secret === undefined) {
|
|
34994
|
+
await ensureReadable(factor.secretCiphertext, newKey, enrollment.userId);
|
|
34995
|
+
return factor;
|
|
34996
|
+
}
|
|
34997
|
+
rotated = true;
|
|
34998
|
+
return {
|
|
34999
|
+
...factor,
|
|
35000
|
+
secretCiphertext: await encryptTotpSecret(secret, newKey)
|
|
35001
|
+
};
|
|
35002
|
+
}));
|
|
35003
|
+
if (!rotated) {
|
|
34793
35004
|
const plan2 = { kind: "already" };
|
|
34794
35005
|
return plan2;
|
|
34795
35006
|
}
|
|
34796
35007
|
const plan = {
|
|
34797
|
-
enrollment: {
|
|
34798
|
-
...enrollment,
|
|
34799
|
-
totpSecretCiphertext: await encryptTotpSecret(secret, newKey),
|
|
34800
|
-
updatedAt: Date.now()
|
|
34801
|
-
},
|
|
35008
|
+
enrollment: withMfaFactors({ ...enrollment, updatedAt: Date.now() }, nextFactors),
|
|
34802
35009
|
kind: "rotate"
|
|
34803
35010
|
};
|
|
34804
35011
|
return plan;
|
|
@@ -34821,7 +35028,8 @@ var rotateMfaEncryptionKey = async ({
|
|
|
34821
35028
|
// src/mfa/inMemoryMfaStore.ts
|
|
34822
35029
|
var cloneEnrollment = (value) => ({
|
|
34823
35030
|
...value,
|
|
34824
|
-
backupCodeHashes: [...value.backupCodeHashes]
|
|
35031
|
+
backupCodeHashes: [...value.backupCodeHashes],
|
|
35032
|
+
factors: value.factors?.map((factor) => ({ ...factor }))
|
|
34825
35033
|
});
|
|
34826
35034
|
var createInMemoryMfaStore = () => {
|
|
34827
35035
|
const enrollments = new Map;
|
|
@@ -34843,6 +35051,7 @@ var createInMemoryMfaStore = () => {
|
|
|
34843
35051
|
},
|
|
34844
35052
|
completeSmsChallenge: async ({
|
|
34845
35053
|
challengeId,
|
|
35054
|
+
factors,
|
|
34846
35055
|
lastUsedAt,
|
|
34847
35056
|
smsVerified,
|
|
34848
35057
|
userId
|
|
@@ -34852,11 +35061,13 @@ var createInMemoryMfaStore = () => {
|
|
|
34852
35061
|
return false;
|
|
34853
35062
|
enrollments.set(userId, cloneEnrollment({
|
|
34854
35063
|
...current,
|
|
35064
|
+
factors: factors ?? current.factors,
|
|
34855
35065
|
lastUsedAt: lastUsedAt ?? current.lastUsedAt,
|
|
34856
35066
|
smsChallengeId: undefined,
|
|
34857
35067
|
smsFailedAttempts: 0,
|
|
34858
35068
|
smsPendingCodeExpiresAt: undefined,
|
|
34859
35069
|
smsPendingCodeHash: undefined,
|
|
35070
|
+
smsPendingFactorId: undefined,
|
|
34860
35071
|
smsPendingPurpose: undefined,
|
|
34861
35072
|
smsProviderReference: undefined,
|
|
34862
35073
|
smsVerified,
|
|
@@ -34917,6 +35128,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
34917
35128
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
34918
35129
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
34919
35130
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
35131
|
+
mfa_factors: jsonb("mfa_factors").$type(),
|
|
34920
35132
|
sms_challenge_id: text("sms_challenge_id"),
|
|
34921
35133
|
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
34922
35134
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
@@ -34924,6 +35136,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
34924
35136
|
mode: "number"
|
|
34925
35137
|
}),
|
|
34926
35138
|
sms_pending_code_hash: text("sms_pending_code_hash"),
|
|
35139
|
+
sms_pending_factor_id: text("sms_pending_factor_id"),
|
|
34927
35140
|
sms_pending_purpose: text("sms_pending_purpose").$type(),
|
|
34928
35141
|
sms_phone: varchar("sms_phone", { length: PHONE_LENGTH }),
|
|
34929
35142
|
sms_provider_reference: text("sms_provider_reference"),
|
|
@@ -34937,12 +35150,14 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
34937
35150
|
var toEnrollment = (row) => ({
|
|
34938
35151
|
backupCodeHashes: row.backup_code_hashes,
|
|
34939
35152
|
createdAt: row.created_at_ms,
|
|
35153
|
+
factors: row.mfa_factors ?? undefined,
|
|
34940
35154
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
34941
35155
|
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
34942
35156
|
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
34943
35157
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
34944
35158
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
34945
35159
|
smsPendingCodeHash: row.sms_pending_code_hash ?? undefined,
|
|
35160
|
+
smsPendingFactorId: row.sms_pending_factor_id ?? undefined,
|
|
34946
35161
|
smsPendingPurpose: row.sms_pending_purpose ?? undefined,
|
|
34947
35162
|
smsPhone: row.sms_phone ?? undefined,
|
|
34948
35163
|
smsProviderReference: row.sms_provider_reference ?? undefined,
|
|
@@ -34959,11 +35174,13 @@ var createPostgresMfaStore = (db) => {
|
|
|
34959
35174
|
backup_code_hashes: enrollment.backupCodeHashes,
|
|
34960
35175
|
created_at_ms: enrollment.createdAt,
|
|
34961
35176
|
last_used_at_ms: enrollment.lastUsedAt ?? null,
|
|
35177
|
+
mfa_factors: enrollment.factors ?? null,
|
|
34962
35178
|
sms_challenge_id: enrollment.smsChallengeId ?? null,
|
|
34963
35179
|
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
34964
35180
|
sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
|
|
34965
35181
|
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
34966
35182
|
sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
|
|
35183
|
+
sms_pending_factor_id: enrollment.smsPendingFactorId ?? null,
|
|
34967
35184
|
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
34968
35185
|
sms_phone: enrollment.smsPhone ?? null,
|
|
34969
35186
|
sms_provider_reference: enrollment.smsProviderReference ?? null,
|
|
@@ -34986,11 +35203,13 @@ var createPostgresMfaStore = (db) => {
|
|
|
34986
35203
|
});
|
|
34987
35204
|
const rows = await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
34988
35205
|
set: {
|
|
35206
|
+
mfa_factors: enrollment.factors ?? null,
|
|
34989
35207
|
sms_challenge_id: challengeId,
|
|
34990
35208
|
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
34991
35209
|
sms_failed_attempts: 0,
|
|
34992
35210
|
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
34993
35211
|
sms_pending_code_hash: null,
|
|
35212
|
+
sms_pending_factor_id: enrollment.smsPendingFactorId ?? null,
|
|
34994
35213
|
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
34995
35214
|
sms_phone: enrollment.smsPhone ?? null,
|
|
34996
35215
|
sms_provider_reference: null,
|
|
@@ -35004,16 +35223,19 @@ var createPostgresMfaStore = (db) => {
|
|
|
35004
35223
|
},
|
|
35005
35224
|
completeSmsChallenge: async ({
|
|
35006
35225
|
challengeId,
|
|
35226
|
+
factors,
|
|
35007
35227
|
lastUsedAt,
|
|
35008
35228
|
smsVerified,
|
|
35009
35229
|
userId
|
|
35010
35230
|
}) => {
|
|
35011
35231
|
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
35012
35232
|
last_used_at_ms: lastUsedAt,
|
|
35233
|
+
mfa_factors: factors,
|
|
35013
35234
|
sms_challenge_id: null,
|
|
35014
35235
|
sms_failed_attempts: 0,
|
|
35015
35236
|
sms_pending_code_expires_at_ms: null,
|
|
35016
35237
|
sms_pending_code_hash: null,
|
|
35238
|
+
sms_pending_factor_id: null,
|
|
35017
35239
|
sms_pending_purpose: null,
|
|
35018
35240
|
sms_provider_reference: null,
|
|
35019
35241
|
sms_verified: smsVerified,
|
|
@@ -39553,6 +39775,14 @@ var mfaSmsAtomicChallengeMigration = {
|
|
|
39553
39775
|
id: "0005_sms_atomic_challenge",
|
|
39554
39776
|
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_challenge_id" text;'
|
|
39555
39777
|
};
|
|
39778
|
+
var mfaMultipleFactorsMigration = {
|
|
39779
|
+
id: "0006_multiple_factors",
|
|
39780
|
+
sql: [
|
|
39781
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "mfa_factors" jsonb;',
|
|
39782
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_pending_factor_id" text;'
|
|
39783
|
+
].join(`
|
|
39784
|
+
`)
|
|
39785
|
+
};
|
|
39556
39786
|
var oidcResourceAudienceMigration = {
|
|
39557
39787
|
id: "0002_resource_audience",
|
|
39558
39788
|
sql: [
|
|
@@ -39634,7 +39864,8 @@ var blockMigrations = {
|
|
|
39634
39864
|
mfaSmsColumnsMigration,
|
|
39635
39865
|
mfaTotpLockoutMigration,
|
|
39636
39866
|
mfaSmsDeliveryPolicyMigration,
|
|
39637
|
-
mfaSmsAtomicChallengeMigration
|
|
39867
|
+
mfaSmsAtomicChallengeMigration,
|
|
39868
|
+
mfaMultipleFactorsMigration
|
|
39638
39869
|
]
|
|
39639
39870
|
},
|
|
39640
39871
|
oidc: {
|
|
@@ -40500,6 +40731,7 @@ var createAuthApplications = async (configuration) => buildAuthApplications(conf
|
|
|
40500
40731
|
export {
|
|
40501
40732
|
writeWarrant,
|
|
40502
40733
|
withSpan,
|
|
40734
|
+
withMfaFactors,
|
|
40503
40735
|
withAbsoluteNativeAuthClients,
|
|
40504
40736
|
webhookDeliveriesTable,
|
|
40505
40737
|
webauthnRoutes,
|
|
@@ -40708,6 +40940,7 @@ export {
|
|
|
40708
40940
|
getStatus,
|
|
40709
40941
|
getRegisteredClient,
|
|
40710
40942
|
getOrRefreshFederatedTokens,
|
|
40943
|
+
getMfaFactors,
|
|
40711
40944
|
getCredentialStatus,
|
|
40712
40945
|
generateTotpSecret,
|
|
40713
40946
|
generateTotp,
|
|
@@ -41026,5 +41259,5 @@ export {
|
|
|
41026
41259
|
ABSOLUTE_NATIVE_AUTH_CLIENTS_ENV
|
|
41027
41260
|
};
|
|
41028
41261
|
|
|
41029
|
-
//# debugId=
|
|
41262
|
+
//# debugId=8C9E5A777004332D64756E2164756E21
|
|
41030
41263
|
//# sourceMappingURL=index.js.map
|