@absolutejs/auth 0.75.7 → 0.76.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/dist/agents/context.d.ts +2 -6
- package/dist/agents/index.js +5 -6
- package/dist/agents/index.js.map +3 -3
- package/dist/agents/routes.d.ts +4 -10
- package/dist/authContext.d.ts +66 -178
- package/dist/authorization/protectPermission.d.ts +3 -8
- package/dist/cli/migrate.js +19 -15
- package/dist/cli/migrate.js.map +5 -5
- package/dist/htmx/configuredRoutes.d.ts +37 -126
- package/dist/htmx/routes.d.ts +37 -126
- package/dist/index.d.ts +237 -431
- package/dist/index.js +332 -113
- package/dist/index.js.map +17 -17
- 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/routes/protectRoute.d.ts +4 -21
- package/dist/routes/stepUp.d.ts +3 -8
- package/dist/server.js +330 -113
- package/dist/server.js.map +17 -17
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3372,8 +3372,8 @@ var failureResponse = (config, failure, requiredScopes) => new Response(JSON.str
|
|
|
3372
3372
|
var agentAuthContextPlugin = (config) => new Elysia2({
|
|
3373
3373
|
name: "@absolutejs/auth/agent-context",
|
|
3374
3374
|
seed: pluginDependencySeed(config)
|
|
3375
|
-
}).derive(({ request }) => {
|
|
3376
|
-
|
|
3375
|
+
}).derive(({ request }) => ({
|
|
3376
|
+
protectAgent: async (requiredScopes, handleAuth, handleAuthFail) => {
|
|
3377
3377
|
if (config === undefined) {
|
|
3378
3378
|
const failure = {
|
|
3379
3379
|
code: "Unauthorized",
|
|
@@ -3399,9 +3399,8 @@ var agentAuthContextPlugin = (config) => new Elysia2({
|
|
|
3399
3399
|
return await handleAuthFail?.(failure) ?? failureResponse(config, failure, requiredScopes);
|
|
3400
3400
|
}
|
|
3401
3401
|
return handleAuth(principal);
|
|
3402
|
-
}
|
|
3403
|
-
|
|
3404
|
-
}).as("global");
|
|
3402
|
+
}
|
|
3403
|
+
})).as("global");
|
|
3405
3404
|
|
|
3406
3405
|
// src/agents/oauthGuide.ts
|
|
3407
3406
|
var DEFAULT_GUIDE_ROUTE = "/auth.md";
|
|
@@ -4612,17 +4611,17 @@ var protectPermissionPlugin = ({
|
|
|
4612
4611
|
}).use(sessionStore()).guard({
|
|
4613
4612
|
cookie: t3.Cookie({ user_session_id: userSessionIdTypebox }),
|
|
4614
4613
|
schema: "merge"
|
|
4615
|
-
}).derive(({ store: { session }, cookie: { user_session_id }, status }) => {
|
|
4616
|
-
|
|
4614
|
+
}).derive(({ store: { session }, cookie: { user_session_id }, status }) => ({
|
|
4615
|
+
protectPermission: (check, handleAuth, handleAuthFail) => getStatusFromSource({
|
|
4617
4616
|
authSessionStore,
|
|
4618
4617
|
session,
|
|
4619
4618
|
user_session_id
|
|
4620
4619
|
}).then(async ({ user, error }) => {
|
|
4621
4620
|
if (error) {
|
|
4622
|
-
return
|
|
4621
|
+
return handleAuthFail?.(error) ?? status(error.code, error.message);
|
|
4623
4622
|
}
|
|
4624
4623
|
if (!user) {
|
|
4625
|
-
return
|
|
4624
|
+
return handleAuthFail?.({
|
|
4626
4625
|
code: "Unauthorized",
|
|
4627
4626
|
message: "User is not authenticated"
|
|
4628
4627
|
}) ?? status("Unauthorized", "User is not authenticated");
|
|
@@ -4639,15 +4638,14 @@ var protectPermissionPlugin = ({
|
|
|
4639
4638
|
organizationId: check.organizationId,
|
|
4640
4639
|
type: "authorization_denied"
|
|
4641
4640
|
});
|
|
4642
|
-
return
|
|
4641
|
+
return handleAuthFail?.({
|
|
4643
4642
|
code: "Forbidden",
|
|
4644
4643
|
message: "Insufficient permissions"
|
|
4645
4644
|
}) ?? status("Forbidden", "Insufficient permissions");
|
|
4646
4645
|
}
|
|
4647
4646
|
return handleAuth(user);
|
|
4648
|
-
})
|
|
4649
|
-
|
|
4650
|
-
}).as("global");
|
|
4647
|
+
})
|
|
4648
|
+
})).as("global");
|
|
4651
4649
|
|
|
4652
4650
|
// src/routes/protectRoute.ts
|
|
4653
4651
|
import { Elysia as Elysia6, t as t4 } from "elysia";
|
|
@@ -4711,7 +4709,8 @@ var protectRoutePlugin = ({
|
|
|
4711
4709
|
}).derive(async ({
|
|
4712
4710
|
store: { session },
|
|
4713
4711
|
cookie: { user_session_id },
|
|
4714
|
-
headers
|
|
4712
|
+
headers,
|
|
4713
|
+
status
|
|
4715
4714
|
}) => {
|
|
4716
4715
|
const sessionStatus = await getStatusFromSource({
|
|
4717
4716
|
authSessionStore,
|
|
@@ -4730,30 +4729,25 @@ var protectRoutePlugin = ({
|
|
|
4730
4729
|
authorization: headers.authorization,
|
|
4731
4730
|
config: accessTokens
|
|
4732
4731
|
});
|
|
4733
|
-
return {
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
return
|
|
4750
|
-
code: "Unauthorized",
|
|
4751
|
-
message: "User is not authenticated"
|
|
4752
|
-
}) ?? status("Unauthorized", "User is not authenticated");
|
|
4732
|
+
return {
|
|
4733
|
+
authPrincipal,
|
|
4734
|
+
protectRoute: (handleAuth, handleAuthFail) => {
|
|
4735
|
+
const user = authPrincipal?.user;
|
|
4736
|
+
const { error } = sessionStatus;
|
|
4737
|
+
if (error) {
|
|
4738
|
+
if (user)
|
|
4739
|
+
return handleAuth(user);
|
|
4740
|
+
return handleAuthFail?.(error) ?? status(error.code, error.message);
|
|
4741
|
+
}
|
|
4742
|
+
if (!user) {
|
|
4743
|
+
return handleAuthFail?.({
|
|
4744
|
+
code: "Unauthorized",
|
|
4745
|
+
message: "User is not authenticated"
|
|
4746
|
+
}) ?? status("Unauthorized", "User is not authenticated");
|
|
4747
|
+
}
|
|
4748
|
+
return handleAuth(user);
|
|
4753
4749
|
}
|
|
4754
|
-
return handleAuth(user);
|
|
4755
4750
|
};
|
|
4756
|
-
return { protectRoute };
|
|
4757
4751
|
}).as("global");
|
|
4758
4752
|
|
|
4759
4753
|
// src/routes/stepUp.ts
|
|
@@ -4766,24 +4760,23 @@ var stepUpPlugin = ({
|
|
|
4766
4760
|
}).use(sessionStore()).guard({
|
|
4767
4761
|
cookie: t5.Cookie({ user_session_id: userSessionIdTypebox }),
|
|
4768
4762
|
schema: "merge"
|
|
4769
|
-
}).derive(({ store: { session }, cookie: { user_session_id }, status }) => {
|
|
4770
|
-
|
|
4763
|
+
}).derive(({ store: { session }, cookie: { user_session_id }, status }) => ({
|
|
4764
|
+
requireRecentAuth: (maxAgeMs, handleAuth, handleAuthFail) => loadSessionFromSource({
|
|
4771
4765
|
authSessionStore,
|
|
4772
4766
|
session,
|
|
4773
4767
|
userSessionId: user_session_id.value
|
|
4774
|
-
}).then(
|
|
4768
|
+
}).then((userSession) => {
|
|
4775
4769
|
const authenticatedAt = userSession?.authenticatedAt;
|
|
4776
4770
|
const isRecent = authenticatedAt !== undefined && Date.now() - authenticatedAt <= maxAgeMs;
|
|
4777
4771
|
if (!userSession || !isRecent) {
|
|
4778
|
-
return
|
|
4772
|
+
return handleAuthFail?.({
|
|
4779
4773
|
code: "Unauthorized",
|
|
4780
4774
|
message: "Recent authentication required"
|
|
4781
4775
|
}) ?? status("Unauthorized", "Recent authentication required");
|
|
4782
4776
|
}
|
|
4783
4777
|
return handleAuth(userSession.user);
|
|
4784
|
-
})
|
|
4785
|
-
|
|
4786
|
-
}).as("global");
|
|
4778
|
+
})
|
|
4779
|
+
})).as("global");
|
|
4787
4780
|
|
|
4788
4781
|
// src/oidc/socketTickets.ts
|
|
4789
4782
|
init_crypto();
|
|
@@ -6194,7 +6187,45 @@ var createLockoutGuard = ({
|
|
|
6194
6187
|
});
|
|
6195
6188
|
|
|
6196
6189
|
// src/mfa/types.ts
|
|
6197
|
-
var
|
|
6190
|
+
var LEGACY_SMS_FACTOR_ID = "legacy-sms";
|
|
6191
|
+
var LEGACY_TOTP_FACTOR_ID = "legacy-totp";
|
|
6192
|
+
var getMfaFactors = (enrollment) => {
|
|
6193
|
+
if (enrollment.factors !== undefined)
|
|
6194
|
+
return enrollment.factors;
|
|
6195
|
+
const factors = [];
|
|
6196
|
+
if (enrollment.totpSecretCiphertext) {
|
|
6197
|
+
factors.push({
|
|
6198
|
+
id: LEGACY_TOTP_FACTOR_ID,
|
|
6199
|
+
label: "Authenticator app",
|
|
6200
|
+
secretCiphertext: enrollment.totpSecretCiphertext,
|
|
6201
|
+
type: "totp",
|
|
6202
|
+
verified: enrollment.totpVerified
|
|
6203
|
+
});
|
|
6204
|
+
}
|
|
6205
|
+
if (enrollment.smsPhone) {
|
|
6206
|
+
factors.push({
|
|
6207
|
+
id: LEGACY_SMS_FACTOR_ID,
|
|
6208
|
+
label: "Text message",
|
|
6209
|
+
phone: enrollment.smsPhone,
|
|
6210
|
+
type: "sms",
|
|
6211
|
+
verified: enrollment.smsVerified
|
|
6212
|
+
});
|
|
6213
|
+
}
|
|
6214
|
+
return factors;
|
|
6215
|
+
};
|
|
6216
|
+
var isMfaEnrolled = (enrollment) => enrollment !== undefined && (getMfaFactors(enrollment).some((factor) => factor.verified) || enrollment.backupCodeHashes.length > 0);
|
|
6217
|
+
var withMfaFactors = (enrollment, factors) => {
|
|
6218
|
+
const firstSms = factors.find((factor) => factor.type === "sms");
|
|
6219
|
+
const firstTotp = factors.find((factor) => factor.type === "totp");
|
|
6220
|
+
return {
|
|
6221
|
+
...enrollment,
|
|
6222
|
+
factors,
|
|
6223
|
+
smsPhone: firstSms?.phone,
|
|
6224
|
+
smsVerified: firstSms?.verified ?? false,
|
|
6225
|
+
totpSecretCiphertext: firstTotp?.secretCiphertext,
|
|
6226
|
+
totpVerified: firstTotp?.verified ?? false
|
|
6227
|
+
};
|
|
6228
|
+
};
|
|
6198
6229
|
|
|
6199
6230
|
// src/mfa/gate.ts
|
|
6200
6231
|
var createMfaGate = ({ getUserId, mfaStore }) => async (user) => isMfaEnrolled(await mfaStore.getEnrollment(getUserId(user)));
|
|
@@ -6410,6 +6441,8 @@ var hasRecentAuthentication = (session, maxAgeMs, now = Date.now()) => session.a
|
|
|
6410
6441
|
// src/mfa/sms.ts
|
|
6411
6442
|
var DECIMAL_RADIX2 = 10;
|
|
6412
6443
|
var MASK_VISIBLE_DIGITS = 4;
|
|
6444
|
+
var FACTOR_LABEL_MAX_LENGTH = 80;
|
|
6445
|
+
var DEFAULT_SMS_LABEL = "Text message";
|
|
6413
6446
|
var E164_PATTERN = /^\+[1-9]\d{7,14}$/u;
|
|
6414
6447
|
var generateNumericCode = (length) => {
|
|
6415
6448
|
const bytes = new Uint8Array(length);
|
|
@@ -6583,10 +6616,13 @@ var mfaSmsRoutes = ({
|
|
|
6583
6616
|
smsSetupRoute = "/auth/mfa/sms/setup",
|
|
6584
6617
|
smsVerifyRoute = "/auth/mfa/sms/verify"
|
|
6585
6618
|
}) => new Elysia18().use(sessionStore()).post(smsSetupRoute, {
|
|
6586
|
-
body: t12.Object({
|
|
6619
|
+
body: t12.Object({
|
|
6620
|
+
label: t12.Optional(t12.String({ maxLength: FACTOR_LABEL_MAX_LENGTH })),
|
|
6621
|
+
phone: t12.String()
|
|
6622
|
+
}),
|
|
6587
6623
|
cookie: t12.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6588
6624
|
}, async ({
|
|
6589
|
-
body: { phone },
|
|
6625
|
+
body: { label, phone },
|
|
6590
6626
|
cookie: { user_session_id },
|
|
6591
6627
|
status,
|
|
6592
6628
|
store: { session }
|
|
@@ -6614,21 +6650,35 @@ var mfaSmsRoutes = ({
|
|
|
6614
6650
|
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
6615
6651
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6616
6652
|
}
|
|
6653
|
+
const base = existing ?? {
|
|
6654
|
+
backupCodeHashes: [],
|
|
6655
|
+
createdAt: now,
|
|
6656
|
+
smsVerified: false,
|
|
6657
|
+
totpVerified: false,
|
|
6658
|
+
updatedAt: now,
|
|
6659
|
+
userId
|
|
6660
|
+
};
|
|
6661
|
+
const factor = {
|
|
6662
|
+
id: crypto.randomUUID(),
|
|
6663
|
+
label: label?.trim() || DEFAULT_SMS_LABEL,
|
|
6664
|
+
phone,
|
|
6665
|
+
type: "sms",
|
|
6666
|
+
verified: false
|
|
6667
|
+
};
|
|
6668
|
+
const factors = [
|
|
6669
|
+
...getMfaFactors(base).filter((existingFactor) => existingFactor.type !== "sms" || existingFactor.verified),
|
|
6670
|
+
factor
|
|
6671
|
+
];
|
|
6617
6672
|
try {
|
|
6618
6673
|
await issueAndStoreSmsCode({
|
|
6619
6674
|
codeLength: smsCodeLength,
|
|
6620
6675
|
enrollment: {
|
|
6621
|
-
|
|
6622
|
-
createdAt: existing?.createdAt ?? now,
|
|
6623
|
-
lastUsedAt: existing?.lastUsedAt,
|
|
6676
|
+
...withMfaFactors(base, factors),
|
|
6624
6677
|
smsFailedAttempts: 0,
|
|
6678
|
+
smsPendingFactorId: factor.id,
|
|
6625
6679
|
smsPhone: phone,
|
|
6626
|
-
smsVerified: false,
|
|
6627
|
-
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
6628
|
-
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
6629
|
-
totpVerified: existing?.totpVerified ?? false,
|
|
6630
6680
|
updatedAt: now,
|
|
6631
|
-
userId
|
|
6681
|
+
userId: base.userId
|
|
6632
6682
|
},
|
|
6633
6683
|
mfaStore,
|
|
6634
6684
|
onSendSmsCode,
|
|
@@ -6645,12 +6695,18 @@ var mfaSmsRoutes = ({
|
|
|
6645
6695
|
throw error;
|
|
6646
6696
|
return status(mapped.status, mapped.message);
|
|
6647
6697
|
}
|
|
6648
|
-
return status("OK", {
|
|
6698
|
+
return status("OK", {
|
|
6699
|
+
factorId: factor.id,
|
|
6700
|
+
phone: maskPhone(phone)
|
|
6701
|
+
});
|
|
6649
6702
|
}).post(smsVerifyRoute, {
|
|
6650
|
-
body: t12.Object({
|
|
6703
|
+
body: t12.Object({
|
|
6704
|
+
code: t12.String(),
|
|
6705
|
+
factorId: t12.Optional(t12.String())
|
|
6706
|
+
}),
|
|
6651
6707
|
cookie: t12.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6652
6708
|
}, async ({
|
|
6653
|
-
body: { code },
|
|
6709
|
+
body: { code, factorId },
|
|
6654
6710
|
cookie: { user_session_id },
|
|
6655
6711
|
status,
|
|
6656
6712
|
store: { session }
|
|
@@ -6671,6 +6727,12 @@ var mfaSmsRoutes = ({
|
|
|
6671
6727
|
if (!enrollment?.smsPhone) {
|
|
6672
6728
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
6673
6729
|
}
|
|
6730
|
+
const factors = getMfaFactors(enrollment);
|
|
6731
|
+
const pendingFactorId = factorId ?? enrollment.smsPendingFactorId;
|
|
6732
|
+
const pendingFactor = factors.find((candidate) => candidate.type === "sms" && (candidate.id === pendingFactorId || pendingFactorId === undefined && candidate.phone === enrollment.smsPhone));
|
|
6733
|
+
if (!pendingFactor || pendingFactor.type !== "sms") {
|
|
6734
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
6735
|
+
}
|
|
6674
6736
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6675
6737
|
const challengeId = enrollment.smsChallengeId;
|
|
6676
6738
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
@@ -6698,7 +6760,7 @@ var mfaSmsRoutes = ({
|
|
|
6698
6760
|
purpose: "mfa_enrollment",
|
|
6699
6761
|
reference: providerReference,
|
|
6700
6762
|
subject: userId,
|
|
6701
|
-
to:
|
|
6763
|
+
to: pendingFactor.phone
|
|
6702
6764
|
});
|
|
6703
6765
|
if (checked.error !== undefined) {
|
|
6704
6766
|
return status(checked.error.status, checked.error.message);
|
|
@@ -6714,8 +6776,10 @@ var mfaSmsRoutes = ({
|
|
|
6714
6776
|
});
|
|
6715
6777
|
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
|
|
6716
6778
|
}
|
|
6779
|
+
const verifiedFactors = factors.map((candidate) => candidate.id === pendingFactor.id ? { ...candidate, verified: true } : candidate);
|
|
6717
6780
|
const completed = await mfaStore.completeSmsChallenge({
|
|
6718
6781
|
challengeId,
|
|
6782
|
+
factors: verifiedFactors,
|
|
6719
6783
|
smsVerified: true,
|
|
6720
6784
|
userId
|
|
6721
6785
|
});
|
|
@@ -6745,15 +6809,50 @@ var mfaChallenge = ({
|
|
|
6745
6809
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
6746
6810
|
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
6747
6811
|
totpMaxAttempts = DEFAULT_TOTP_MAX_ATTEMPTS
|
|
6748
|
-
}) => new Elysia19().use(sessionStore()).
|
|
6812
|
+
}) => new Elysia19().use(sessionStore()).get(challengeRoute, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) }, async ({
|
|
6813
|
+
cookie: { user_session_id },
|
|
6814
|
+
status,
|
|
6815
|
+
store: { unregisteredSession }
|
|
6816
|
+
}) => {
|
|
6817
|
+
const compatibilityLayer = await createSessionCompatibilityLayer({
|
|
6818
|
+
authSessionStore,
|
|
6819
|
+
userSessionId: user_session_id.value
|
|
6820
|
+
});
|
|
6821
|
+
const challengeUnregistered = authSessionStore ? compatibilityLayer.unregisteredSession : unregisteredSession;
|
|
6822
|
+
const pendingId = user_session_id.value;
|
|
6823
|
+
const pending = pendingId ? challengeUnregistered[pendingId] : undefined;
|
|
6824
|
+
if (!pending) {
|
|
6825
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
6826
|
+
}
|
|
6827
|
+
const user = await getChallengeUser(pending.userIdentity ?? {});
|
|
6828
|
+
const enrollment = user ? await mfaStore.getEnrollment(getUserId(user)) : undefined;
|
|
6829
|
+
if (!enrollment) {
|
|
6830
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
6831
|
+
}
|
|
6832
|
+
const response = {
|
|
6833
|
+
backupCodesAvailable: enrollment.backupCodeHashes.length > 0,
|
|
6834
|
+
factors: getMfaFactors(enrollment).filter((factor) => factor.verified).map((factor) => ({
|
|
6835
|
+
id: factor.id,
|
|
6836
|
+
label: factor.label,
|
|
6837
|
+
phone: factor.type === "sms" ? maskPhone(factor.phone) : null,
|
|
6838
|
+
type: factor.type
|
|
6839
|
+
}))
|
|
6840
|
+
};
|
|
6841
|
+
return status("OK", response);
|
|
6842
|
+
}).post(challengeRoute, {
|
|
6749
6843
|
body: t13.Object({
|
|
6750
6844
|
action: t13.Optional(t13.Union([t13.Literal("send"), t13.Literal("verify")])),
|
|
6751
6845
|
code: t13.Optional(t13.String()),
|
|
6752
|
-
factor: t13.Optional(t13.
|
|
6846
|
+
factor: t13.Optional(t13.Union([
|
|
6847
|
+
t13.Literal("backup_codes"),
|
|
6848
|
+
t13.Literal("sms"),
|
|
6849
|
+
t13.Literal("totp")
|
|
6850
|
+
])),
|
|
6851
|
+
factorId: t13.Optional(t13.String())
|
|
6753
6852
|
}),
|
|
6754
6853
|
cookie: t13.Cookie({ user_session_id: userSessionIdTypebox })
|
|
6755
6854
|
}, async ({
|
|
6756
|
-
body: { action, code, factor },
|
|
6855
|
+
body: { action, code, factor, factorId },
|
|
6757
6856
|
cookie: { user_session_id },
|
|
6758
6857
|
status,
|
|
6759
6858
|
store: { session, unregisteredSession }
|
|
@@ -6774,6 +6873,7 @@ var mfaChallenge = ({
|
|
|
6774
6873
|
if (!user || !enrollment) {
|
|
6775
6874
|
return status("Unauthorized", "No MFA challenge in progress");
|
|
6776
6875
|
}
|
|
6876
|
+
const factors = getMfaFactors(enrollment);
|
|
6777
6877
|
const promote = async () => {
|
|
6778
6878
|
delete challengeUnregistered[pendingId];
|
|
6779
6879
|
const userSessionId = crypto.randomUUID();
|
|
@@ -6796,10 +6896,18 @@ var mfaChallenge = ({
|
|
|
6796
6896
|
if (enrollment.smsCodeSentAt !== undefined && Date.now() - enrollment.smsCodeSentAt < smsResendCooldownMs) {
|
|
6797
6897
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6798
6898
|
}
|
|
6899
|
+
const smsFactor = factors.find((candidate) => candidate.type === "sms" && candidate.verified && (candidate.id === factorId || factorId === undefined));
|
|
6900
|
+
if (!smsFactor || smsFactor.type !== "sms") {
|
|
6901
|
+
return status("Bad Request", "SMS factor not found");
|
|
6902
|
+
}
|
|
6799
6903
|
try {
|
|
6800
6904
|
await issueAndStoreSmsCode({
|
|
6801
6905
|
codeLength: smsCodeLength,
|
|
6802
|
-
enrollment
|
|
6906
|
+
enrollment: {
|
|
6907
|
+
...withMfaFactors(enrollment, factors),
|
|
6908
|
+
smsPendingFactorId: smsFactor.id,
|
|
6909
|
+
smsPhone: smsFactor.phone
|
|
6910
|
+
},
|
|
6803
6911
|
mfaStore,
|
|
6804
6912
|
onSendSmsCode,
|
|
6805
6913
|
previousEnrollment: enrollment,
|
|
@@ -6815,10 +6923,19 @@ var mfaChallenge = ({
|
|
|
6815
6923
|
throw error;
|
|
6816
6924
|
return status(mapped.status, mapped.message);
|
|
6817
6925
|
}
|
|
6818
|
-
return status("OK", {
|
|
6926
|
+
return status("OK", {
|
|
6927
|
+
factorId: smsFactor.id,
|
|
6928
|
+
phone: maskPhone(smsFactor.phone),
|
|
6929
|
+
status: "sent"
|
|
6930
|
+
});
|
|
6819
6931
|
};
|
|
6820
6932
|
const runSmsChallenge = async () => {
|
|
6821
|
-
if (
|
|
6933
|
+
if (action !== "send" && factorId !== undefined && enrollment.smsPendingFactorId !== undefined && factorId !== enrollment.smsPendingFactorId) {
|
|
6934
|
+
return status("Bad Request", "No SMS code in progress");
|
|
6935
|
+
}
|
|
6936
|
+
const selectedFactorId = enrollment.smsPendingFactorId ?? factorId;
|
|
6937
|
+
const smsFactor = factors.find((candidate) => candidate.type === "sms" && candidate.verified && (candidate.id === selectedFactorId || selectedFactorId === undefined));
|
|
6938
|
+
if (!smsFactor || smsFactor.type !== "sms") {
|
|
6822
6939
|
return status("Unauthorized", "No MFA challenge in progress");
|
|
6823
6940
|
}
|
|
6824
6941
|
if (action === "send")
|
|
@@ -6857,7 +6974,7 @@ var mfaChallenge = ({
|
|
|
6857
6974
|
purpose: "mfa_challenge",
|
|
6858
6975
|
reference: providerReference,
|
|
6859
6976
|
subject: getUserId(user),
|
|
6860
|
-
to:
|
|
6977
|
+
to: smsFactor.phone
|
|
6861
6978
|
});
|
|
6862
6979
|
if (checked.error !== undefined) {
|
|
6863
6980
|
return status(checked.error.status, checked.error.message);
|
|
@@ -6879,6 +6996,7 @@ var mfaChallenge = ({
|
|
|
6879
6996
|
}
|
|
6880
6997
|
const completed = await mfaStore.completeSmsChallenge({
|
|
6881
6998
|
challengeId,
|
|
6999
|
+
factors,
|
|
6882
7000
|
lastUsedAt: Date.now(),
|
|
6883
7001
|
smsVerified: true,
|
|
6884
7002
|
userId: getUserId(user)
|
|
@@ -6900,11 +7018,14 @@ var mfaChallenge = ({
|
|
|
6900
7018
|
});
|
|
6901
7019
|
return status("Unauthorized", "Too many attempts");
|
|
6902
7020
|
}
|
|
6903
|
-
const
|
|
6904
|
-
|
|
7021
|
+
const totpFactors = factors.filter((candidate) => candidate.type === "totp" && candidate.verified && (candidate.id === factorId || factorId === undefined));
|
|
7022
|
+
const totpChecks = factor === "backup_codes" ? [] : await Promise.all(totpFactors.map(async (candidate) => verifyTotp({
|
|
7023
|
+
secret: await decryptTotpSecret(candidate.secretCiphertext, encryptionKey),
|
|
6905
7024
|
token: code
|
|
6906
|
-
})
|
|
6907
|
-
const
|
|
7025
|
+
})));
|
|
7026
|
+
const totpValid = totpChecks.some(Boolean);
|
|
7027
|
+
const mayUseBackupCode = factor === "backup_codes" || factor === undefined && factorId === undefined;
|
|
7028
|
+
const remainingBackupHashes = totpValid || !mayUseBackupCode ? undefined : await consumeBackupCode(code, enrollment.backupCodeHashes);
|
|
6908
7029
|
if (!totpValid && remainingBackupHashes === undefined) {
|
|
6909
7030
|
await mfaStore.saveEnrollment({
|
|
6910
7031
|
...enrollment,
|
|
@@ -6936,6 +7057,12 @@ var maskPhone2 = (phone) => {
|
|
|
6936
7057
|
const suffix = phone.slice(-visibleDigits);
|
|
6937
7058
|
return `${"*".repeat(Math.max(0, phone.length - visibleDigits))}${suffix}`;
|
|
6938
7059
|
};
|
|
7060
|
+
var publicFactors = (enrollment) => enrollment ? getMfaFactors(enrollment).filter((factor) => factor.verified).map((factor) => ({
|
|
7061
|
+
id: factor.id,
|
|
7062
|
+
label: factor.label,
|
|
7063
|
+
phone: factor.type === "sms" ? maskPhone2(factor.phone) : null,
|
|
7064
|
+
type: factor.type
|
|
7065
|
+
})) : [];
|
|
6939
7066
|
var mfaManagementRoutes = ({
|
|
6940
7067
|
authSessionStore,
|
|
6941
7068
|
getUserId,
|
|
@@ -6959,6 +7086,7 @@ var mfaManagementRoutes = ({
|
|
|
6959
7086
|
const response = {
|
|
6960
7087
|
backupCodesRemaining: enrollment?.backupCodeHashes.length ?? 0,
|
|
6961
7088
|
enabled: isMfaEnrolled(enrollment),
|
|
7089
|
+
factors: publicFactors(enrollment),
|
|
6962
7090
|
smsBackup: {
|
|
6963
7091
|
enabled: enrollment?.smsVerified ?? false,
|
|
6964
7092
|
phone: maskPhone2(enrollment?.smsPhone)
|
|
@@ -6984,11 +7112,54 @@ var mfaManagementRoutes = ({
|
|
|
6984
7112
|
}
|
|
6985
7113
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
6986
7114
|
return status("OK", { status: "disabled" });
|
|
7115
|
+
}).delete(`${managementRoute}/factors/:factorId`, {
|
|
7116
|
+
cookie: t14.Cookie({ user_session_id: userSessionIdTypebox }),
|
|
7117
|
+
params: t14.Object({ factorId: t14.String() })
|
|
7118
|
+
}, async ({
|
|
7119
|
+
cookie: { user_session_id },
|
|
7120
|
+
params: { factorId },
|
|
7121
|
+
status,
|
|
7122
|
+
store: { session }
|
|
7123
|
+
}) => {
|
|
7124
|
+
const userSession = await loadSessionFromSource({
|
|
7125
|
+
authSessionStore,
|
|
7126
|
+
session,
|
|
7127
|
+
userSessionId: user_session_id.value
|
|
7128
|
+
});
|
|
7129
|
+
if (!userSession) {
|
|
7130
|
+
return status("Unauthorized", "Authentication required");
|
|
7131
|
+
}
|
|
7132
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7133
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7134
|
+
}
|
|
7135
|
+
const userId = getUserId(userSession.user);
|
|
7136
|
+
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7137
|
+
if (!enrollment) {
|
|
7138
|
+
return status("Not Found", "MFA factor not found");
|
|
7139
|
+
}
|
|
7140
|
+
const factors = getMfaFactors(enrollment);
|
|
7141
|
+
if (!factors.some((factor) => factor.id === factorId)) {
|
|
7142
|
+
return status("Not Found", "MFA factor not found");
|
|
7143
|
+
}
|
|
7144
|
+
const remaining = factors.filter((factor) => factor.id !== factorId);
|
|
7145
|
+
if (remaining.length === 0) {
|
|
7146
|
+
await mfaStore.removeEnrollment(userId);
|
|
7147
|
+
} else {
|
|
7148
|
+
const hasVerifiedFactor = remaining.some((factor) => factor.verified);
|
|
7149
|
+
await mfaStore.saveEnrollment(withMfaFactors({
|
|
7150
|
+
...enrollment,
|
|
7151
|
+
backupCodeHashes: hasVerifiedFactor ? enrollment.backupCodeHashes : [],
|
|
7152
|
+
updatedAt: Date.now()
|
|
7153
|
+
}, remaining));
|
|
7154
|
+
}
|
|
7155
|
+
return status("OK", { status: "removed" });
|
|
6987
7156
|
});
|
|
6988
7157
|
|
|
6989
7158
|
// src/mfa/totp.ts
|
|
6990
7159
|
init_crypto();
|
|
6991
7160
|
import { Elysia as Elysia21, t as t15 } from "elysia";
|
|
7161
|
+
var FACTOR_LABEL_MAX_LENGTH2 = 80;
|
|
7162
|
+
var DEFAULT_TOTP_LABEL = "Authenticator app";
|
|
6992
7163
|
var mfaTotpRoutes = ({
|
|
6993
7164
|
authSessionStore,
|
|
6994
7165
|
backupCodeCount = DEFAULT_BACKUP_CODE_COUNT,
|
|
@@ -7000,7 +7171,13 @@ var mfaTotpRoutes = ({
|
|
|
7000
7171
|
onMfaEnrolled,
|
|
7001
7172
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
7002
7173
|
totpVerifyRoute = "/auth/mfa/totp/verify"
|
|
7003
|
-
}) => new Elysia21().use(sessionStore()).post(totpSetupRoute, {
|
|
7174
|
+
}) => new Elysia21().use(sessionStore()).post(totpSetupRoute, {
|
|
7175
|
+
body: t15.Object({
|
|
7176
|
+
label: t15.Optional(t15.String({ maxLength: FACTOR_LABEL_MAX_LENGTH2 }))
|
|
7177
|
+
}),
|
|
7178
|
+
cookie: t15.Cookie({ user_session_id: userSessionIdTypebox })
|
|
7179
|
+
}, async ({
|
|
7180
|
+
body: { label },
|
|
7004
7181
|
cookie: { user_session_id },
|
|
7005
7182
|
status,
|
|
7006
7183
|
store: { session }
|
|
@@ -7020,20 +7197,25 @@ var mfaTotpRoutes = ({
|
|
|
7020
7197
|
const secret = generateTotpSecret();
|
|
7021
7198
|
const existing = await mfaStore.getEnrollment(userId);
|
|
7022
7199
|
const now = Date.now();
|
|
7023
|
-
|
|
7024
|
-
backupCodeHashes:
|
|
7025
|
-
createdAt:
|
|
7026
|
-
|
|
7027
|
-
smsPendingCodeExpiresAt: existing?.smsPendingCodeExpiresAt,
|
|
7028
|
-
smsPendingCodeHash: existing?.smsPendingCodeHash,
|
|
7029
|
-
smsPhone: existing?.smsPhone,
|
|
7030
|
-
smsVerified: existing?.smsVerified ?? false,
|
|
7031
|
-
totpSecretCiphertext: await encryptTotpSecret(secret, encryptionKey),
|
|
7200
|
+
const base = existing ?? {
|
|
7201
|
+
backupCodeHashes: [],
|
|
7202
|
+
createdAt: now,
|
|
7203
|
+
smsVerified: false,
|
|
7032
7204
|
totpVerified: false,
|
|
7033
7205
|
updatedAt: now,
|
|
7034
7206
|
userId
|
|
7035
|
-
}
|
|
7207
|
+
};
|
|
7208
|
+
const factor = {
|
|
7209
|
+
id: crypto.randomUUID(),
|
|
7210
|
+
label: label?.trim() || DEFAULT_TOTP_LABEL,
|
|
7211
|
+
secretCiphertext: await encryptTotpSecret(secret, encryptionKey),
|
|
7212
|
+
type: "totp",
|
|
7213
|
+
verified: false
|
|
7214
|
+
};
|
|
7215
|
+
const factors = getMfaFactors(base).filter((existingFactor) => existingFactor.type !== "totp" || existingFactor.verified);
|
|
7216
|
+
await mfaStore.saveEnrollment(withMfaFactors({ ...base, updatedAt: now }, [...factors, factor]));
|
|
7036
7217
|
return status("OK", {
|
|
7218
|
+
factorId: factor.id,
|
|
7037
7219
|
secret,
|
|
7038
7220
|
uri: createTotpKeyUri({
|
|
7039
7221
|
accountName: userId,
|
|
@@ -7042,10 +7224,13 @@ var mfaTotpRoutes = ({
|
|
|
7042
7224
|
})
|
|
7043
7225
|
});
|
|
7044
7226
|
}).post(totpVerifyRoute, {
|
|
7045
|
-
body: t15.Object({
|
|
7227
|
+
body: t15.Object({
|
|
7228
|
+
code: t15.String(),
|
|
7229
|
+
factorId: t15.Optional(t15.String())
|
|
7230
|
+
}),
|
|
7046
7231
|
cookie: t15.Cookie({ user_session_id: userSessionIdTypebox })
|
|
7047
7232
|
}, async ({
|
|
7048
|
-
body: { code },
|
|
7233
|
+
body: { code, factorId },
|
|
7049
7234
|
cookie: { user_session_id },
|
|
7050
7235
|
status,
|
|
7051
7236
|
store: { session }
|
|
@@ -7063,21 +7248,26 @@ var mfaTotpRoutes = ({
|
|
|
7063
7248
|
}
|
|
7064
7249
|
const userId = getUserId(userSession.user);
|
|
7065
7250
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7066
|
-
if (!enrollment
|
|
7251
|
+
if (!enrollment) {
|
|
7252
|
+
return status("Bad Request", "No TOTP enrollment in progress");
|
|
7253
|
+
}
|
|
7254
|
+
const factors = getMfaFactors(enrollment);
|
|
7255
|
+
const factor = factors.find((candidate) => candidate.type === "totp" && (candidate.id === factorId || factorId === undefined && !candidate.verified));
|
|
7256
|
+
if (!factor || factor.type !== "totp") {
|
|
7067
7257
|
return status("Bad Request", "No TOTP enrollment in progress");
|
|
7068
7258
|
}
|
|
7069
|
-
const secret = await decryptTotpSecret(
|
|
7259
|
+
const secret = await decryptTotpSecret(factor.secretCiphertext, encryptionKey);
|
|
7070
7260
|
const valid = await verifyTotp({ secret, token: code });
|
|
7071
7261
|
if (!valid) {
|
|
7072
7262
|
return status("Bad Request", "Invalid TOTP code");
|
|
7073
7263
|
}
|
|
7074
7264
|
const { codes, hashes } = await generateBackupCodes(backupCodeCount);
|
|
7075
|
-
|
|
7265
|
+
const verifiedFactors = factors.map((candidate) => candidate.id === factor.id ? { ...candidate, verified: true } : candidate);
|
|
7266
|
+
await mfaStore.saveEnrollment(withMfaFactors({
|
|
7076
7267
|
...enrollment,
|
|
7077
7268
|
backupCodeHashes: hashes,
|
|
7078
|
-
totpVerified: true,
|
|
7079
7269
|
updatedAt: Date.now()
|
|
7080
|
-
});
|
|
7270
|
+
}, verifiedFactors));
|
|
7081
7271
|
await onMfaEnrolled?.({ userId });
|
|
7082
7272
|
return status("OK", { backupCodes: codes });
|
|
7083
7273
|
});
|
|
@@ -34952,23 +35142,33 @@ var ensureReadable = async (ciphertext, key, userId) => {
|
|
|
34952
35142
|
}
|
|
34953
35143
|
};
|
|
34954
35144
|
var planEnrollment = async (enrollment, oldKey, newKey) => {
|
|
34955
|
-
const
|
|
34956
|
-
|
|
35145
|
+
const factors = getMfaFactors(enrollment);
|
|
35146
|
+
const totpFactors = factors.filter((factor) => factor.type === "totp");
|
|
35147
|
+
if (totpFactors.length === 0) {
|
|
34957
35148
|
const plan2 = { kind: "skip" };
|
|
34958
35149
|
return plan2;
|
|
34959
35150
|
}
|
|
34960
|
-
|
|
34961
|
-
|
|
34962
|
-
|
|
35151
|
+
let rotated = false;
|
|
35152
|
+
const nextFactors = await Promise.all(factors.map(async (factor) => {
|
|
35153
|
+
if (factor.type !== "totp")
|
|
35154
|
+
return factor;
|
|
35155
|
+
const secret = await tryDecrypt(factor.secretCiphertext, oldKey);
|
|
35156
|
+
if (secret === undefined) {
|
|
35157
|
+
await ensureReadable(factor.secretCiphertext, newKey, enrollment.userId);
|
|
35158
|
+
return factor;
|
|
35159
|
+
}
|
|
35160
|
+
rotated = true;
|
|
35161
|
+
return {
|
|
35162
|
+
...factor,
|
|
35163
|
+
secretCiphertext: await encryptTotpSecret(secret, newKey)
|
|
35164
|
+
};
|
|
35165
|
+
}));
|
|
35166
|
+
if (!rotated) {
|
|
34963
35167
|
const plan2 = { kind: "already" };
|
|
34964
35168
|
return plan2;
|
|
34965
35169
|
}
|
|
34966
35170
|
const plan = {
|
|
34967
|
-
enrollment: {
|
|
34968
|
-
...enrollment,
|
|
34969
|
-
totpSecretCiphertext: await encryptTotpSecret(secret, newKey),
|
|
34970
|
-
updatedAt: Date.now()
|
|
34971
|
-
},
|
|
35171
|
+
enrollment: withMfaFactors({ ...enrollment, updatedAt: Date.now() }, nextFactors),
|
|
34972
35172
|
kind: "rotate"
|
|
34973
35173
|
};
|
|
34974
35174
|
return plan;
|
|
@@ -34991,7 +35191,8 @@ var rotateMfaEncryptionKey = async ({
|
|
|
34991
35191
|
// src/mfa/inMemoryMfaStore.ts
|
|
34992
35192
|
var cloneEnrollment = (value) => ({
|
|
34993
35193
|
...value,
|
|
34994
|
-
backupCodeHashes: [...value.backupCodeHashes]
|
|
35194
|
+
backupCodeHashes: [...value.backupCodeHashes],
|
|
35195
|
+
factors: value.factors?.map((factor) => ({ ...factor }))
|
|
34995
35196
|
});
|
|
34996
35197
|
var createInMemoryMfaStore = () => {
|
|
34997
35198
|
const enrollments = new Map;
|
|
@@ -35013,6 +35214,7 @@ var createInMemoryMfaStore = () => {
|
|
|
35013
35214
|
},
|
|
35014
35215
|
completeSmsChallenge: async ({
|
|
35015
35216
|
challengeId,
|
|
35217
|
+
factors,
|
|
35016
35218
|
lastUsedAt,
|
|
35017
35219
|
smsVerified,
|
|
35018
35220
|
userId
|
|
@@ -35022,11 +35224,13 @@ var createInMemoryMfaStore = () => {
|
|
|
35022
35224
|
return false;
|
|
35023
35225
|
enrollments.set(userId, cloneEnrollment({
|
|
35024
35226
|
...current,
|
|
35227
|
+
factors: factors ?? current.factors,
|
|
35025
35228
|
lastUsedAt: lastUsedAt ?? current.lastUsedAt,
|
|
35026
35229
|
smsChallengeId: undefined,
|
|
35027
35230
|
smsFailedAttempts: 0,
|
|
35028
35231
|
smsPendingCodeExpiresAt: undefined,
|
|
35029
35232
|
smsPendingCodeHash: undefined,
|
|
35233
|
+
smsPendingFactorId: undefined,
|
|
35030
35234
|
smsPendingPurpose: undefined,
|
|
35031
35235
|
smsProviderReference: undefined,
|
|
35032
35236
|
smsVerified,
|
|
@@ -35087,6 +35291,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
35087
35291
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
35088
35292
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
35089
35293
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
35294
|
+
mfa_factors: jsonb("mfa_factors").$type(),
|
|
35090
35295
|
sms_challenge_id: text("sms_challenge_id"),
|
|
35091
35296
|
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
35092
35297
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
@@ -35094,6 +35299,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
35094
35299
|
mode: "number"
|
|
35095
35300
|
}),
|
|
35096
35301
|
sms_pending_code_hash: text("sms_pending_code_hash"),
|
|
35302
|
+
sms_pending_factor_id: text("sms_pending_factor_id"),
|
|
35097
35303
|
sms_pending_purpose: text("sms_pending_purpose").$type(),
|
|
35098
35304
|
sms_phone: varchar("sms_phone", { length: PHONE_LENGTH }),
|
|
35099
35305
|
sms_provider_reference: text("sms_provider_reference"),
|
|
@@ -35107,12 +35313,14 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
35107
35313
|
var toEnrollment = (row) => ({
|
|
35108
35314
|
backupCodeHashes: row.backup_code_hashes,
|
|
35109
35315
|
createdAt: row.created_at_ms,
|
|
35316
|
+
factors: row.mfa_factors ?? undefined,
|
|
35110
35317
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
35111
35318
|
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
35112
35319
|
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
35113
35320
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
35114
35321
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
35115
35322
|
smsPendingCodeHash: row.sms_pending_code_hash ?? undefined,
|
|
35323
|
+
smsPendingFactorId: row.sms_pending_factor_id ?? undefined,
|
|
35116
35324
|
smsPendingPurpose: row.sms_pending_purpose ?? undefined,
|
|
35117
35325
|
smsPhone: row.sms_phone ?? undefined,
|
|
35118
35326
|
smsProviderReference: row.sms_provider_reference ?? undefined,
|
|
@@ -35129,11 +35337,13 @@ var createPostgresMfaStore = (db) => {
|
|
|
35129
35337
|
backup_code_hashes: enrollment.backupCodeHashes,
|
|
35130
35338
|
created_at_ms: enrollment.createdAt,
|
|
35131
35339
|
last_used_at_ms: enrollment.lastUsedAt ?? null,
|
|
35340
|
+
mfa_factors: enrollment.factors ?? null,
|
|
35132
35341
|
sms_challenge_id: enrollment.smsChallengeId ?? null,
|
|
35133
35342
|
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
35134
35343
|
sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
|
|
35135
35344
|
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
35136
35345
|
sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
|
|
35346
|
+
sms_pending_factor_id: enrollment.smsPendingFactorId ?? null,
|
|
35137
35347
|
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
35138
35348
|
sms_phone: enrollment.smsPhone ?? null,
|
|
35139
35349
|
sms_provider_reference: enrollment.smsProviderReference ?? null,
|
|
@@ -35156,11 +35366,13 @@ var createPostgresMfaStore = (db) => {
|
|
|
35156
35366
|
});
|
|
35157
35367
|
const rows = await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
35158
35368
|
set: {
|
|
35369
|
+
mfa_factors: enrollment.factors ?? null,
|
|
35159
35370
|
sms_challenge_id: challengeId,
|
|
35160
35371
|
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
35161
35372
|
sms_failed_attempts: 0,
|
|
35162
35373
|
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
35163
35374
|
sms_pending_code_hash: null,
|
|
35375
|
+
sms_pending_factor_id: enrollment.smsPendingFactorId ?? null,
|
|
35164
35376
|
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
35165
35377
|
sms_phone: enrollment.smsPhone ?? null,
|
|
35166
35378
|
sms_provider_reference: null,
|
|
@@ -35174,16 +35386,19 @@ var createPostgresMfaStore = (db) => {
|
|
|
35174
35386
|
},
|
|
35175
35387
|
completeSmsChallenge: async ({
|
|
35176
35388
|
challengeId,
|
|
35389
|
+
factors,
|
|
35177
35390
|
lastUsedAt,
|
|
35178
35391
|
smsVerified,
|
|
35179
35392
|
userId
|
|
35180
35393
|
}) => {
|
|
35181
35394
|
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
35182
35395
|
last_used_at_ms: lastUsedAt,
|
|
35396
|
+
mfa_factors: factors,
|
|
35183
35397
|
sms_challenge_id: null,
|
|
35184
35398
|
sms_failed_attempts: 0,
|
|
35185
35399
|
sms_pending_code_expires_at_ms: null,
|
|
35186
35400
|
sms_pending_code_hash: null,
|
|
35401
|
+
sms_pending_factor_id: null,
|
|
35187
35402
|
sms_pending_purpose: null,
|
|
35188
35403
|
sms_provider_reference: null,
|
|
35189
35404
|
sms_verified: smsVerified,
|
|
@@ -39577,12 +39792,7 @@ var renderChunk = (chunk) => {
|
|
|
39577
39792
|
return "";
|
|
39578
39793
|
};
|
|
39579
39794
|
var formatSqlTemplate = (value) => value.queryChunks.map(renderChunk).join("");
|
|
39580
|
-
var
|
|
39581
|
-
const dimensions = Reflect.get(column, "dimensions");
|
|
39582
|
-
const elementType = column.getSQLType();
|
|
39583
|
-
return typeof dimensions === "number" && dimensions > 0 ? `${elementType}${"[]".repeat(dimensions)}` : elementType;
|
|
39584
|
-
};
|
|
39585
|
-
var formatScalarDefault = (value) => {
|
|
39795
|
+
var formatDefault = (value) => {
|
|
39586
39796
|
if (value === null || value === undefined)
|
|
39587
39797
|
return "NULL";
|
|
39588
39798
|
if (is(value, SQL))
|
|
@@ -39593,17 +39803,15 @@ var formatScalarDefault = (value) => {
|
|
|
39593
39803
|
return value ? "true" : "false";
|
|
39594
39804
|
if (typeof value === "number")
|
|
39595
39805
|
return String(value);
|
|
39596
|
-
if (typeof value === "object") {
|
|
39806
|
+
if (Array.isArray(value) || typeof value === "object") {
|
|
39597
39807
|
return `'${JSON.stringify(value)}'::jsonb`;
|
|
39598
39808
|
}
|
|
39599
39809
|
return String(value);
|
|
39600
39810
|
};
|
|
39601
|
-
var
|
|
39602
|
-
var formatDefault = (column, value) => {
|
|
39811
|
+
var columnSqlType = (column) => {
|
|
39603
39812
|
const dimensions = Reflect.get(column, "dimensions");
|
|
39604
|
-
|
|
39605
|
-
|
|
39606
|
-
return formatScalarDefault(value);
|
|
39813
|
+
const elementType = column.getSQLType();
|
|
39814
|
+
return typeof dimensions === "number" && dimensions > 0 ? `${elementType}${"[]".repeat(dimensions)}` : elementType;
|
|
39607
39815
|
};
|
|
39608
39816
|
var columnSql = (column) => {
|
|
39609
39817
|
const parts = [`"${column.name}"`, columnSqlType(column)];
|
|
@@ -39612,7 +39820,7 @@ var columnSql = (column) => {
|
|
|
39612
39820
|
if (column.notNull && !column.primary)
|
|
39613
39821
|
parts.push("NOT NULL");
|
|
39614
39822
|
if (column.hasDefault && column.default !== undefined) {
|
|
39615
|
-
parts.push(`DEFAULT ${formatDefault(column
|
|
39823
|
+
parts.push(`DEFAULT ${formatDefault(column.default)}`);
|
|
39616
39824
|
}
|
|
39617
39825
|
if (column.isUnique)
|
|
39618
39826
|
parts.push("UNIQUE");
|
|
@@ -39730,6 +39938,14 @@ var mfaSmsAtomicChallengeMigration = {
|
|
|
39730
39938
|
id: "0005_sms_atomic_challenge",
|
|
39731
39939
|
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_challenge_id" text;'
|
|
39732
39940
|
};
|
|
39941
|
+
var mfaMultipleFactorsMigration = {
|
|
39942
|
+
id: "0006_multiple_factors",
|
|
39943
|
+
sql: [
|
|
39944
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "mfa_factors" jsonb;',
|
|
39945
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_pending_factor_id" text;'
|
|
39946
|
+
].join(`
|
|
39947
|
+
`)
|
|
39948
|
+
};
|
|
39733
39949
|
var oidcResourceAudienceMigration = {
|
|
39734
39950
|
id: "0002_resource_audience",
|
|
39735
39951
|
sql: [
|
|
@@ -39811,7 +40027,8 @@ var blockMigrations = {
|
|
|
39811
40027
|
mfaSmsColumnsMigration,
|
|
39812
40028
|
mfaTotpLockoutMigration,
|
|
39813
40029
|
mfaSmsDeliveryPolicyMigration,
|
|
39814
|
-
mfaSmsAtomicChallengeMigration
|
|
40030
|
+
mfaSmsAtomicChallengeMigration,
|
|
40031
|
+
mfaMultipleFactorsMigration
|
|
39815
40032
|
]
|
|
39816
40033
|
},
|
|
39817
40034
|
oidc: {
|
|
@@ -40991,6 +41208,7 @@ export {
|
|
|
40991
41208
|
generateTotp,
|
|
40992
41209
|
generateTotpSecret,
|
|
40993
41210
|
getCredentialStatus,
|
|
41211
|
+
getMfaFactors,
|
|
40994
41212
|
getOrRefreshFederatedTokens,
|
|
40995
41213
|
getRegisteredClient,
|
|
40996
41214
|
getStatus,
|
|
@@ -41199,9 +41417,10 @@ export {
|
|
|
41199
41417
|
webauthnRoutes,
|
|
41200
41418
|
webhookDeliveriesTable,
|
|
41201
41419
|
withAbsoluteNativeAuthClients,
|
|
41420
|
+
withMfaFactors,
|
|
41202
41421
|
withSpan,
|
|
41203
41422
|
writeWarrant
|
|
41204
41423
|
};
|
|
41205
41424
|
|
|
41206
|
-
//# debugId=
|
|
41425
|
+
//# debugId=3F694839DFE8077C64756E2164756E21
|
|
41207
41426
|
//# sourceMappingURL=index.js.map
|