@absolutejs/auth 0.57.9 → 0.59.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/README.md +38 -0
- package/dist/cli/migrate.js +31 -12
- package/dist/cli/migrate.js.map +4 -4
- package/dist/index.js +522 -95
- package/dist/index.js.map +17 -15
- package/dist/manifest.js +8 -1
- package/dist/manifest.js.map +3 -3
- package/dist/mfa/challenge.d.ts +5 -3
- package/dist/mfa/config.d.ts +9 -0
- package/dist/mfa/management.d.ts +3 -3
- package/dist/mfa/postgresMfaStore.d.ts +60 -0
- package/dist/mfa/recentAuth.d.ts +2 -0
- package/dist/mfa/routes.d.ts +15 -10
- package/dist/mfa/sms.d.ts +45 -7
- package/dist/mfa/totp.d.ts +3 -3
- package/dist/mfa/types.d.ts +35 -0
- package/dist/server.js +521 -96
- package/dist/server.js.map +18 -16
- package/dist/types.d.ts +5 -1
- package/dist/verification/types.d.ts +58 -0
- package/package.json +1 -1
- package/dist/agents/index.d.ts +0 -13
- package/dist/cli/import/auth0.d.ts +0 -2
- package/dist/cli/import/clerk.d.ts +0 -2
- package/dist/cli/import/index.d.ts +0 -10
- package/dist/cli/import/lucia.d.ts +0 -2
- package/dist/cli/import/nextauth.d.ts +0 -2
- package/dist/cli/import/supabase.d.ts +0 -2
- package/dist/cli/import/types.d.ts +0 -27
- package/dist/cli/migrate.d.ts +0 -2
- package/dist/client/components/react/SignIn.d.ts +0 -32
- package/dist/client/components/react/SignUp.d.ts +0 -29
- package/dist/client/components/react/UserButton.d.ts +0 -36
- package/dist/client/createAuthClient.d.ts +0 -285
- package/dist/client/index.d.ts +0 -3
- package/dist/client/passkeyHelpers.d.ts +0 -19
- package/dist/client/react.d.ts +0 -87
- package/dist/client/sessionExpiry.d.ts +0 -37
- package/dist/client/solid.d.ts +0 -86
- package/dist/client/svelte.d.ts +0 -86
- package/dist/client/vue.d.ts +0 -88
- package/dist/fingerprint-client/index.d.ts +0 -26
- package/dist/htmx/index.d.ts +0 -2
- package/dist/index.d.ts +0 -3637
- package/dist/linkedProviders/index.d.ts +0 -3
- package/dist/manifest.d.ts +0 -18
- package/dist/manifest.json +0 -364
- package/dist/oidc/index.d.ts +0 -12
- package/dist/oidc/operator.d.ts +0 -14
- package/dist/plugins/denyDisposableEmail.d.ts +0 -7
- package/dist/plugins/discordAlert.d.ts +0 -7
- package/dist/plugins/geoBlock.d.ts +0 -8
- package/dist/plugins/index.d.ts +0 -12
- package/dist/plugins/pagerdutyAlert.d.ts +0 -9
- package/dist/plugins/posthogIdentify.d.ts +0 -7
- package/dist/plugins/slackAlert.d.ts +0 -7
- package/dist/providers/index.d.ts +0 -2
- package/dist/saml.d.ts +0 -1
- package/dist/server.d.ts +0 -31
- package/dist/sso/nodeSamlAdapter.d.ts +0 -8
- package/dist/vault/index.d.ts +0 -5
- package/dist/webauthn/simpleWebAuthnAdapter.d.ts +0 -3
- package/dist/webauthn.d.ts +0 -1
package/dist/index.js
CHANGED
|
@@ -5848,11 +5848,13 @@ var generateBackupCodes = async (count) => {
|
|
|
5848
5848
|
init_constants();
|
|
5849
5849
|
var DEFAULT_BACKUP_CODE_COUNT = 10;
|
|
5850
5850
|
var DEFAULT_MFA_ISSUER = "AbsoluteAuth";
|
|
5851
|
+
var DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS = 5 * SECONDS_IN_A_MINUTE * MILLISECONDS_IN_A_SECOND;
|
|
5851
5852
|
var DEFAULT_MFA_SESSION_TTL_MS = MILLISECONDS_IN_A_DAY;
|
|
5852
5853
|
var DEFAULT_SMS_CODE_LENGTH = 6;
|
|
5853
5854
|
var SMS_CODE_TTL_MINUTES = 5;
|
|
5854
5855
|
var DEFAULT_SMS_CODE_TTL_MS = SMS_CODE_TTL_MINUTES * SECONDS_IN_A_MINUTE * MILLISECONDS_IN_A_SECOND;
|
|
5855
5856
|
var DEFAULT_SMS_MAX_ATTEMPTS = 3;
|
|
5857
|
+
var DEFAULT_SMS_RESEND_COOLDOWN_MS = 30 * MILLISECONDS_IN_A_SECOND;
|
|
5856
5858
|
var DEFAULT_TOTP_MAX_ATTEMPTS = 5;
|
|
5857
5859
|
|
|
5858
5860
|
// src/mfa/secret.ts
|
|
@@ -5863,6 +5865,23 @@ var encryptTotpSecret = (secret, encryptionKey) => encryptionKey ? encryptSecret
|
|
|
5863
5865
|
// src/mfa/sms.ts
|
|
5864
5866
|
init_crypto();
|
|
5865
5867
|
import { Elysia as Elysia17, t as t11 } from "elysia";
|
|
5868
|
+
|
|
5869
|
+
// src/verification/types.ts
|
|
5870
|
+
class VerificationProviderError extends Error {
|
|
5871
|
+
kind;
|
|
5872
|
+
provider;
|
|
5873
|
+
constructor(input) {
|
|
5874
|
+
super(input.message, { cause: input.cause });
|
|
5875
|
+
this.name = "VerificationProviderError";
|
|
5876
|
+
this.kind = input.kind;
|
|
5877
|
+
this.provider = input.provider;
|
|
5878
|
+
}
|
|
5879
|
+
}
|
|
5880
|
+
|
|
5881
|
+
// src/mfa/recentAuth.ts
|
|
5882
|
+
var hasRecentAuthentication = (session, maxAgeMs, now = Date.now()) => session.authenticatedAt !== undefined && now >= session.authenticatedAt && now - session.authenticatedAt <= maxAgeMs;
|
|
5883
|
+
|
|
5884
|
+
// src/mfa/sms.ts
|
|
5866
5885
|
var DECIMAL_RADIX2 = 10;
|
|
5867
5886
|
var MASK_VISIBLE_DIGITS = 4;
|
|
5868
5887
|
var E164_PATTERN = /^\+[1-9]\d{7,14}$/u;
|
|
@@ -5877,28 +5896,147 @@ var issueSmsCode = async (codeLength, ttlMs) => {
|
|
|
5877
5896
|
const expiresAt = Date.now() + ttlMs;
|
|
5878
5897
|
return { code, expiresAt, hash };
|
|
5879
5898
|
};
|
|
5899
|
+
var startProviderSmsChallenge = async (input) => {
|
|
5900
|
+
let started;
|
|
5901
|
+
try {
|
|
5902
|
+
started = await input.verificationProvider.start({
|
|
5903
|
+
channel: "sms",
|
|
5904
|
+
purpose: input.purpose,
|
|
5905
|
+
subject: input.userId,
|
|
5906
|
+
to: input.phone
|
|
5907
|
+
});
|
|
5908
|
+
} catch (error) {
|
|
5909
|
+
await input.mfaStore.rollbackSmsChallenge({
|
|
5910
|
+
challengeId: input.challengeId,
|
|
5911
|
+
previous: input.previousEnrollment,
|
|
5912
|
+
userId: input.userId
|
|
5913
|
+
});
|
|
5914
|
+
throw error;
|
|
5915
|
+
}
|
|
5916
|
+
const finalized = await input.mfaStore.finalizeSmsChallenge({
|
|
5917
|
+
challengeId: input.challengeId,
|
|
5918
|
+
expiresAt: started.expiresAt,
|
|
5919
|
+
providerReference: started.reference,
|
|
5920
|
+
userId: input.userId
|
|
5921
|
+
});
|
|
5922
|
+
if (finalized)
|
|
5923
|
+
return started;
|
|
5924
|
+
await input.verificationProvider.cancel({
|
|
5925
|
+
channel: "sms",
|
|
5926
|
+
purpose: input.purpose,
|
|
5927
|
+
reference: started.reference,
|
|
5928
|
+
subject: input.userId,
|
|
5929
|
+
to: input.phone
|
|
5930
|
+
}).catch(() => {
|
|
5931
|
+
return;
|
|
5932
|
+
});
|
|
5933
|
+
throw new SmsChallengeConflictError("SMS challenge claim was lost");
|
|
5934
|
+
};
|
|
5935
|
+
|
|
5936
|
+
class SmsChallengeConflictError extends Error {
|
|
5937
|
+
name = "SmsChallengeConflictError";
|
|
5938
|
+
}
|
|
5939
|
+
var checkWithVerificationProvider = async (provider, input) => {
|
|
5940
|
+
try {
|
|
5941
|
+
return { result: await provider.check(input) };
|
|
5942
|
+
} catch (error) {
|
|
5943
|
+
const mapped = mapVerificationProviderError(error);
|
|
5944
|
+
if (mapped === undefined)
|
|
5945
|
+
throw error;
|
|
5946
|
+
return { error: mapped };
|
|
5947
|
+
}
|
|
5948
|
+
};
|
|
5880
5949
|
var isE164Phone = (phone) => E164_PATTERN.test(phone);
|
|
5881
5950
|
var issueAndStoreSmsCode = async ({
|
|
5882
5951
|
codeLength,
|
|
5883
5952
|
enrollment,
|
|
5884
5953
|
mfaStore,
|
|
5885
5954
|
onSendSmsCode,
|
|
5886
|
-
|
|
5955
|
+
previousEnrollment,
|
|
5956
|
+
purpose,
|
|
5957
|
+
resendCooldownMs,
|
|
5958
|
+
verificationProvider,
|
|
5959
|
+
ttlMs,
|
|
5960
|
+
userId
|
|
5887
5961
|
}) => {
|
|
5888
5962
|
const phone = enrollment.smsPhone;
|
|
5889
5963
|
if (phone === undefined)
|
|
5890
5964
|
return;
|
|
5965
|
+
const challengeId = crypto.randomUUID();
|
|
5966
|
+
const now = Date.now();
|
|
5967
|
+
const claimed = await mfaStore.claimSmsChallenge({
|
|
5968
|
+
challengeId,
|
|
5969
|
+
cooldownCutoff: now - resendCooldownMs,
|
|
5970
|
+
enrollment: {
|
|
5971
|
+
...enrollment,
|
|
5972
|
+
smsChallengeId: challengeId,
|
|
5973
|
+
smsCodeSentAt: now,
|
|
5974
|
+
smsFailedAttempts: 0,
|
|
5975
|
+
smsPendingCodeExpiresAt: now + ttlMs,
|
|
5976
|
+
smsPendingCodeHash: undefined,
|
|
5977
|
+
smsPendingPurpose: purpose,
|
|
5978
|
+
smsProviderReference: undefined,
|
|
5979
|
+
updatedAt: now
|
|
5980
|
+
}
|
|
5981
|
+
});
|
|
5982
|
+
if (!claimed)
|
|
5983
|
+
throw new SmsChallengeConflictError("SMS resend cooldown active");
|
|
5984
|
+
if (verificationProvider !== undefined) {
|
|
5985
|
+
const started = await startProviderSmsChallenge({
|
|
5986
|
+
challengeId,
|
|
5987
|
+
mfaStore,
|
|
5988
|
+
phone,
|
|
5989
|
+
previousEnrollment,
|
|
5990
|
+
purpose,
|
|
5991
|
+
userId,
|
|
5992
|
+
verificationProvider
|
|
5993
|
+
});
|
|
5994
|
+
return started.expiresAt;
|
|
5995
|
+
}
|
|
5891
5996
|
const { code, expiresAt, hash } = await issueSmsCode(codeLength, ttlMs);
|
|
5892
|
-
await mfaStore.
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
updatedAt: Date.now()
|
|
5997
|
+
const finalized = await mfaStore.finalizeSmsChallenge({
|
|
5998
|
+
challengeId,
|
|
5999
|
+
expiresAt,
|
|
6000
|
+
hash,
|
|
6001
|
+
userId
|
|
5898
6002
|
});
|
|
5899
|
-
|
|
6003
|
+
if (!finalized)
|
|
6004
|
+
throw new SmsChallengeConflictError("SMS challenge claim was lost");
|
|
6005
|
+
try {
|
|
6006
|
+
await onSendSmsCode?.({ code, expiresAt, phone, purpose, userId });
|
|
6007
|
+
} catch (error) {
|
|
6008
|
+
await mfaStore.rollbackSmsChallenge({
|
|
6009
|
+
challengeId,
|
|
6010
|
+
previous: previousEnrollment,
|
|
6011
|
+
userId
|
|
6012
|
+
});
|
|
6013
|
+
throw error;
|
|
6014
|
+
}
|
|
5900
6015
|
return expiresAt;
|
|
5901
6016
|
};
|
|
6017
|
+
var mapVerificationProviderError = (error) => {
|
|
6018
|
+
if (error instanceof SmsChallengeConflictError) {
|
|
6019
|
+
return { message: error.message, status: "Too Many Requests" };
|
|
6020
|
+
}
|
|
6021
|
+
if (!(error instanceof VerificationProviderError))
|
|
6022
|
+
return;
|
|
6023
|
+
if (error.kind === "rate_limited") {
|
|
6024
|
+
return {
|
|
6025
|
+
message: "Verification provider rate limit reached",
|
|
6026
|
+
status: "Too Many Requests"
|
|
6027
|
+
};
|
|
6028
|
+
}
|
|
6029
|
+
if (error.kind === "invalid_destination") {
|
|
6030
|
+
return {
|
|
6031
|
+
message: "Phone number cannot receive verification codes",
|
|
6032
|
+
status: "Bad Request"
|
|
6033
|
+
};
|
|
6034
|
+
}
|
|
6035
|
+
return {
|
|
6036
|
+
message: "Verification provider unavailable",
|
|
6037
|
+
status: "Service Unavailable"
|
|
6038
|
+
};
|
|
6039
|
+
};
|
|
5902
6040
|
var maskPhone = (phone) => {
|
|
5903
6041
|
const visible = phone.slice(-MASK_VISIBLE_DIGITS);
|
|
5904
6042
|
const maskedLength = Math.max(phone.length - MASK_VISIBLE_DIGITS, 0);
|
|
@@ -5908,11 +6046,14 @@ var mfaSmsRoutes = ({
|
|
|
5908
6046
|
authSessionStore,
|
|
5909
6047
|
getUserId,
|
|
5910
6048
|
mfaStore,
|
|
6049
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
5911
6050
|
onMfaEnrolled,
|
|
5912
6051
|
onSendSmsCode,
|
|
6052
|
+
verificationProvider,
|
|
5913
6053
|
smsCodeLength = DEFAULT_SMS_CODE_LENGTH,
|
|
5914
6054
|
smsCodeTtlMs = DEFAULT_SMS_CODE_TTL_MS,
|
|
5915
6055
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
6056
|
+
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
5916
6057
|
smsSetupRoute = "/auth/mfa/sms/setup",
|
|
5917
6058
|
smsVerifyRoute = "/auth/mfa/sms/verify"
|
|
5918
6059
|
}) => new Elysia17().use(sessionStore()).post(smsSetupRoute, async ({
|
|
@@ -5921,7 +6062,7 @@ var mfaSmsRoutes = ({
|
|
|
5921
6062
|
status,
|
|
5922
6063
|
store: { session }
|
|
5923
6064
|
}) => {
|
|
5924
|
-
if (onSendSmsCode === undefined) {
|
|
6065
|
+
if (onSendSmsCode === undefined && verificationProvider === undefined) {
|
|
5925
6066
|
return status("Not Implemented", "SMS MFA is not configured");
|
|
5926
6067
|
}
|
|
5927
6068
|
const userSession = await loadSessionFromSource({
|
|
@@ -5932,27 +6073,49 @@ var mfaSmsRoutes = ({
|
|
|
5932
6073
|
if (!userSession) {
|
|
5933
6074
|
return status("Unauthorized", "Authentication required");
|
|
5934
6075
|
}
|
|
6076
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6077
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6078
|
+
}
|
|
5935
6079
|
if (!isE164Phone(phone)) {
|
|
5936
6080
|
return status("Bad Request", "Phone must be in E.164 format");
|
|
5937
6081
|
}
|
|
5938
6082
|
const userId = getUserId(userSession.user);
|
|
5939
6083
|
const existing = await mfaStore.getEnrollment(userId);
|
|
5940
6084
|
const now = Date.now();
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
6085
|
+
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
6086
|
+
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6087
|
+
}
|
|
6088
|
+
try {
|
|
6089
|
+
await issueAndStoreSmsCode({
|
|
6090
|
+
codeLength: smsCodeLength,
|
|
6091
|
+
enrollment: {
|
|
6092
|
+
backupCodeHashes: existing?.backupCodeHashes ?? [],
|
|
6093
|
+
createdAt: existing?.createdAt ?? now,
|
|
6094
|
+
lastUsedAt: existing?.lastUsedAt,
|
|
6095
|
+
smsFailedAttempts: 0,
|
|
6096
|
+
smsPhone: phone,
|
|
6097
|
+
smsVerified: false,
|
|
6098
|
+
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
6099
|
+
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
6100
|
+
totpVerified: existing?.totpVerified ?? false,
|
|
6101
|
+
updatedAt: now,
|
|
6102
|
+
userId
|
|
6103
|
+
},
|
|
6104
|
+
mfaStore,
|
|
6105
|
+
onSendSmsCode,
|
|
6106
|
+
previousEnrollment: existing,
|
|
6107
|
+
purpose: "mfa_enrollment",
|
|
6108
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
6109
|
+
ttlMs: smsCodeTtlMs,
|
|
6110
|
+
userId,
|
|
6111
|
+
verificationProvider
|
|
6112
|
+
});
|
|
6113
|
+
} catch (error) {
|
|
6114
|
+
const mapped = mapVerificationProviderError(error);
|
|
6115
|
+
if (mapped === undefined)
|
|
6116
|
+
throw error;
|
|
6117
|
+
return status(mapped.status, mapped.message);
|
|
6118
|
+
}
|
|
5956
6119
|
return status("OK", { phone: maskPhone(phone) });
|
|
5957
6120
|
}, {
|
|
5958
6121
|
body: t11.Object({ phone: t11.String() }),
|
|
@@ -5971,34 +6134,65 @@ var mfaSmsRoutes = ({
|
|
|
5971
6134
|
if (!userSession) {
|
|
5972
6135
|
return status("Unauthorized", "Authentication required");
|
|
5973
6136
|
}
|
|
6137
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6138
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6139
|
+
}
|
|
5974
6140
|
const userId = getUserId(userSession.user);
|
|
5975
6141
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
5976
|
-
if (!enrollment?.
|
|
6142
|
+
if (!enrollment?.smsPhone) {
|
|
6143
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
6144
|
+
}
|
|
6145
|
+
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6146
|
+
const challengeId = enrollment.smsChallengeId;
|
|
6147
|
+
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
6148
|
+
const providerReference = enrollment.smsProviderReference;
|
|
6149
|
+
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
5977
6150
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
5978
6151
|
}
|
|
5979
|
-
if (
|
|
6152
|
+
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
6153
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
6154
|
+
}
|
|
6155
|
+
if (Date.now() > localCodeExpiresAt) {
|
|
5980
6156
|
return status("Bad Request", "SMS code expired");
|
|
5981
6157
|
}
|
|
5982
6158
|
if ((enrollment.smsFailedAttempts ?? 0) >= smsMaxAttempts) {
|
|
5983
6159
|
return status("Too Many Requests", "Too many attempts");
|
|
5984
6160
|
}
|
|
5985
|
-
|
|
6161
|
+
let providerResult;
|
|
6162
|
+
if (verificationProvider !== undefined) {
|
|
6163
|
+
if (providerReference === undefined) {
|
|
6164
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
6165
|
+
}
|
|
6166
|
+
const checked = await checkWithVerificationProvider(verificationProvider, {
|
|
6167
|
+
channel: "sms",
|
|
6168
|
+
code,
|
|
6169
|
+
purpose: "mfa_enrollment",
|
|
6170
|
+
reference: providerReference,
|
|
6171
|
+
subject: userId,
|
|
6172
|
+
to: enrollment.smsPhone
|
|
6173
|
+
});
|
|
6174
|
+
if (checked.error !== undefined) {
|
|
6175
|
+
return status(checked.error.status, checked.error.message);
|
|
6176
|
+
}
|
|
6177
|
+
providerResult = checked.result;
|
|
6178
|
+
}
|
|
6179
|
+
const codeValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
5986
6180
|
if (!codeValid) {
|
|
5987
|
-
await mfaStore.
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
6181
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
6182
|
+
challengeId,
|
|
6183
|
+
maxAttempts: smsMaxAttempts,
|
|
6184
|
+
userId
|
|
5991
6185
|
});
|
|
5992
|
-
return status("Bad Request", "Invalid SMS code");
|
|
6186
|
+
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
|
|
5993
6187
|
}
|
|
5994
|
-
await mfaStore.
|
|
5995
|
-
|
|
5996
|
-
smsFailedAttempts: 0,
|
|
5997
|
-
smsPendingCodeExpiresAt: undefined,
|
|
5998
|
-
smsPendingCodeHash: undefined,
|
|
6188
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
6189
|
+
challengeId,
|
|
5999
6190
|
smsVerified: true,
|
|
6000
|
-
|
|
6191
|
+
userId
|
|
6001
6192
|
});
|
|
6193
|
+
if (!completed) {
|
|
6194
|
+
return status("Bad Request", "SMS challenge is no longer active");
|
|
6195
|
+
}
|
|
6002
6196
|
await onMfaEnrolled?.({ userId });
|
|
6003
6197
|
return status("OK", { status: "enrolled" });
|
|
6004
6198
|
}, {
|
|
@@ -6018,10 +6212,12 @@ var mfaChallenge = ({
|
|
|
6018
6212
|
onMfaChallengeError,
|
|
6019
6213
|
onMfaChallengeSuccess,
|
|
6020
6214
|
onSendSmsCode,
|
|
6215
|
+
verificationProvider,
|
|
6021
6216
|
sessionDurationMs = DEFAULT_MFA_SESSION_TTL_MS,
|
|
6022
6217
|
smsCodeLength = DEFAULT_SMS_CODE_LENGTH,
|
|
6023
6218
|
smsCodeTtlMs = DEFAULT_SMS_CODE_TTL_MS,
|
|
6024
6219
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
6220
|
+
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
6025
6221
|
totpMaxAttempts = DEFAULT_TOTP_MAX_ATTEMPTS
|
|
6026
6222
|
}) => new Elysia18().use(sessionStore()).post(challengeRoute, async ({
|
|
6027
6223
|
body: { action, code, factor },
|
|
@@ -6063,27 +6259,51 @@ var mfaChallenge = ({
|
|
|
6063
6259
|
await onMfaChallengeSuccess?.({ user, userSessionId });
|
|
6064
6260
|
return status("OK", { status: "authenticated" });
|
|
6065
6261
|
};
|
|
6066
|
-
const
|
|
6067
|
-
if (
|
|
6068
|
-
return status("
|
|
6262
|
+
const sendSmsChallenge = async () => {
|
|
6263
|
+
if (enrollment.smsCodeSentAt !== undefined && Date.now() - enrollment.smsCodeSentAt < smsResendCooldownMs) {
|
|
6264
|
+
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6069
6265
|
}
|
|
6070
|
-
|
|
6266
|
+
try {
|
|
6071
6267
|
await issueAndStoreSmsCode({
|
|
6072
6268
|
codeLength: smsCodeLength,
|
|
6073
6269
|
enrollment,
|
|
6074
6270
|
mfaStore,
|
|
6075
6271
|
onSendSmsCode,
|
|
6076
|
-
|
|
6272
|
+
previousEnrollment: enrollment,
|
|
6273
|
+
purpose: "mfa_challenge",
|
|
6274
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
6275
|
+
ttlMs: smsCodeTtlMs,
|
|
6276
|
+
userId: getUserId(user),
|
|
6277
|
+
verificationProvider
|
|
6077
6278
|
});
|
|
6078
|
-
|
|
6279
|
+
} catch (error) {
|
|
6280
|
+
const mapped = mapVerificationProviderError(error);
|
|
6281
|
+
if (mapped === undefined)
|
|
6282
|
+
throw error;
|
|
6283
|
+
return status(mapped.status, mapped.message);
|
|
6079
6284
|
}
|
|
6285
|
+
return status("OK", { status: "sent" });
|
|
6286
|
+
};
|
|
6287
|
+
const runSmsChallenge = async () => {
|
|
6288
|
+
if (!enrollment.smsVerified || !enrollment.smsPhone) {
|
|
6289
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
6290
|
+
}
|
|
6291
|
+
if (action === "send")
|
|
6292
|
+
return sendSmsChallenge();
|
|
6080
6293
|
if (code === undefined) {
|
|
6081
6294
|
return status("Bad Request", "SMS code required");
|
|
6082
6295
|
}
|
|
6083
|
-
|
|
6296
|
+
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6297
|
+
const challengeId = enrollment.smsChallengeId;
|
|
6298
|
+
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
6299
|
+
const providerReference = enrollment.smsProviderReference;
|
|
6300
|
+
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
6301
|
+
return status("Bad Request", "No SMS code in progress");
|
|
6302
|
+
}
|
|
6303
|
+
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
6084
6304
|
return status("Bad Request", "No SMS code in progress");
|
|
6085
6305
|
}
|
|
6086
|
-
if (Date.now() >
|
|
6306
|
+
if (Date.now() > localCodeExpiresAt) {
|
|
6087
6307
|
return status("Unauthorized", "SMS code expired");
|
|
6088
6308
|
}
|
|
6089
6309
|
if ((enrollment.smsFailedAttempts ?? 0) >= smsMaxAttempts) {
|
|
@@ -6093,27 +6313,46 @@ var mfaChallenge = ({
|
|
|
6093
6313
|
});
|
|
6094
6314
|
return status("Unauthorized", "Too many attempts");
|
|
6095
6315
|
}
|
|
6096
|
-
|
|
6316
|
+
let providerResult;
|
|
6317
|
+
if (verificationProvider !== undefined) {
|
|
6318
|
+
if (providerReference === undefined) {
|
|
6319
|
+
return status("Bad Request", "No SMS code in progress");
|
|
6320
|
+
}
|
|
6321
|
+
const checked = await checkWithVerificationProvider(verificationProvider, {
|
|
6322
|
+
channel: "sms",
|
|
6323
|
+
code,
|
|
6324
|
+
purpose: "mfa_challenge",
|
|
6325
|
+
reference: providerReference,
|
|
6326
|
+
subject: getUserId(user),
|
|
6327
|
+
to: enrollment.smsPhone
|
|
6328
|
+
});
|
|
6329
|
+
if (checked.error !== undefined) {
|
|
6330
|
+
return status(checked.error.status, checked.error.message);
|
|
6331
|
+
}
|
|
6332
|
+
providerResult = checked.result;
|
|
6333
|
+
}
|
|
6334
|
+
const smsValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
6097
6335
|
if (!smsValid) {
|
|
6098
|
-
await mfaStore.
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6336
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
6337
|
+
challengeId,
|
|
6338
|
+
maxAttempts: smsMaxAttempts,
|
|
6339
|
+
userId: getUserId(user)
|
|
6102
6340
|
});
|
|
6103
6341
|
await onMfaChallengeError?.({
|
|
6104
6342
|
error: new Error("invalid_mfa_code"),
|
|
6105
6343
|
userId: getUserId(user)
|
|
6106
6344
|
});
|
|
6107
|
-
return status("Unauthorized", "Invalid MFA code");
|
|
6345
|
+
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "Invalid MFA code");
|
|
6108
6346
|
}
|
|
6109
|
-
await mfaStore.
|
|
6110
|
-
|
|
6347
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
6348
|
+
challengeId,
|
|
6111
6349
|
lastUsedAt: Date.now(),
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
smsPendingCodeHash: undefined,
|
|
6115
|
-
updatedAt: Date.now()
|
|
6350
|
+
smsVerified: true,
|
|
6351
|
+
userId: getUserId(user)
|
|
6116
6352
|
});
|
|
6353
|
+
if (!completed) {
|
|
6354
|
+
return status("Unauthorized", "SMS challenge is no longer active");
|
|
6355
|
+
}
|
|
6117
6356
|
return promote();
|
|
6118
6357
|
};
|
|
6119
6358
|
if (factor === "sms")
|
|
@@ -6175,6 +6414,7 @@ var mfaManagementRoutes = ({
|
|
|
6175
6414
|
authSessionStore,
|
|
6176
6415
|
getUserId,
|
|
6177
6416
|
managementRoute = "/auth/mfa",
|
|
6417
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6178
6418
|
mfaStore
|
|
6179
6419
|
}) => new Elysia19().use(sessionStore()).get(managementRoute, async ({
|
|
6180
6420
|
cookie: { user_session_id },
|
|
@@ -6213,6 +6453,9 @@ var mfaManagementRoutes = ({
|
|
|
6213
6453
|
if (!userSession) {
|
|
6214
6454
|
return status("Unauthorized", "Authentication required");
|
|
6215
6455
|
}
|
|
6456
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6457
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6458
|
+
}
|
|
6216
6459
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
6217
6460
|
return status("OK", { status: "disabled" });
|
|
6218
6461
|
}, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) });
|
|
@@ -6226,6 +6469,7 @@ var mfaTotpRoutes = ({
|
|
|
6226
6469
|
encryptionKey,
|
|
6227
6470
|
getUserId,
|
|
6228
6471
|
issuer = DEFAULT_MFA_ISSUER,
|
|
6472
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6229
6473
|
mfaStore,
|
|
6230
6474
|
onMfaEnrolled,
|
|
6231
6475
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
@@ -6243,6 +6487,9 @@ var mfaTotpRoutes = ({
|
|
|
6243
6487
|
if (!userSession) {
|
|
6244
6488
|
return status("Unauthorized", "Authentication required");
|
|
6245
6489
|
}
|
|
6490
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6491
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6492
|
+
}
|
|
6246
6493
|
const userId = getUserId(userSession.user);
|
|
6247
6494
|
const secret = generateTotpSecret();
|
|
6248
6495
|
const existing = await mfaStore.getEnrollment(userId);
|
|
@@ -6282,6 +6529,9 @@ var mfaTotpRoutes = ({
|
|
|
6282
6529
|
if (!userSession) {
|
|
6283
6530
|
return status("Unauthorized", "Authentication required");
|
|
6284
6531
|
}
|
|
6532
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6533
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6534
|
+
}
|
|
6285
6535
|
const userId = getUserId(userSession.user);
|
|
6286
6536
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
6287
6537
|
if (!enrollment?.totpSecretCiphertext) {
|
|
@@ -31820,14 +32070,85 @@ var cloneEnrollment = (value) => ({
|
|
|
31820
32070
|
var createInMemoryMfaStore = () => {
|
|
31821
32071
|
const enrollments = new Map;
|
|
31822
32072
|
return {
|
|
32073
|
+
claimSmsChallenge: async ({
|
|
32074
|
+
challengeId,
|
|
32075
|
+
cooldownCutoff,
|
|
32076
|
+
enrollment
|
|
32077
|
+
}) => {
|
|
32078
|
+
const current = enrollments.get(enrollment.userId);
|
|
32079
|
+
if (current?.smsCodeSentAt !== undefined && current.smsCodeSentAt > cooldownCutoff) {
|
|
32080
|
+
return false;
|
|
32081
|
+
}
|
|
32082
|
+
enrollments.set(enrollment.userId, cloneEnrollment({
|
|
32083
|
+
...enrollment,
|
|
32084
|
+
smsChallengeId: challengeId
|
|
32085
|
+
}));
|
|
32086
|
+
return true;
|
|
32087
|
+
},
|
|
32088
|
+
completeSmsChallenge: async ({
|
|
32089
|
+
challengeId,
|
|
32090
|
+
lastUsedAt,
|
|
32091
|
+
smsVerified,
|
|
32092
|
+
userId
|
|
32093
|
+
}) => {
|
|
32094
|
+
const current = enrollments.get(userId);
|
|
32095
|
+
if (current?.smsChallengeId !== challengeId)
|
|
32096
|
+
return false;
|
|
32097
|
+
enrollments.set(userId, cloneEnrollment({
|
|
32098
|
+
...current,
|
|
32099
|
+
lastUsedAt: lastUsedAt ?? current.lastUsedAt,
|
|
32100
|
+
smsChallengeId: undefined,
|
|
32101
|
+
smsFailedAttempts: 0,
|
|
32102
|
+
smsPendingCodeExpiresAt: undefined,
|
|
32103
|
+
smsPendingCodeHash: undefined,
|
|
32104
|
+
smsPendingPurpose: undefined,
|
|
32105
|
+
smsProviderReference: undefined,
|
|
32106
|
+
smsVerified,
|
|
32107
|
+
updatedAt: Date.now()
|
|
32108
|
+
}));
|
|
32109
|
+
return true;
|
|
32110
|
+
},
|
|
32111
|
+
finalizeSmsChallenge: async (input) => {
|
|
32112
|
+
const current = enrollments.get(input.userId);
|
|
32113
|
+
if (current?.smsChallengeId !== input.challengeId)
|
|
32114
|
+
return false;
|
|
32115
|
+
enrollments.set(input.userId, cloneEnrollment({
|
|
32116
|
+
...current,
|
|
32117
|
+
smsPendingCodeExpiresAt: input.expiresAt,
|
|
32118
|
+
smsPendingCodeHash: input.hash,
|
|
32119
|
+
smsProviderReference: input.providerReference,
|
|
32120
|
+
updatedAt: Date.now()
|
|
32121
|
+
}));
|
|
32122
|
+
return true;
|
|
32123
|
+
},
|
|
31823
32124
|
getEnrollment: async (userId) => {
|
|
31824
32125
|
const enrollment = enrollments.get(userId);
|
|
31825
32126
|
return enrollment ? cloneEnrollment(enrollment) : undefined;
|
|
31826
32127
|
},
|
|
31827
32128
|
listEnrollments: async () => Array.from(enrollments.values()).map(cloneEnrollment),
|
|
32129
|
+
recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
|
|
32130
|
+
const current = enrollments.get(userId);
|
|
32131
|
+
if (current?.smsChallengeId !== challengeId || (current.smsFailedAttempts ?? 0) >= maxAttempts)
|
|
32132
|
+
return;
|
|
32133
|
+
const attempts = (current.smsFailedAttempts ?? 0) + 1;
|
|
32134
|
+
enrollments.set(userId, cloneEnrollment({
|
|
32135
|
+
...current,
|
|
32136
|
+
smsFailedAttempts: attempts,
|
|
32137
|
+
updatedAt: Date.now()
|
|
32138
|
+
}));
|
|
32139
|
+
return attempts;
|
|
32140
|
+
},
|
|
31828
32141
|
removeEnrollment: async (userId) => {
|
|
31829
32142
|
enrollments.delete(userId);
|
|
31830
32143
|
},
|
|
32144
|
+
rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
|
|
32145
|
+
if (enrollments.get(userId)?.smsChallengeId !== challengeId)
|
|
32146
|
+
return;
|
|
32147
|
+
if (previous)
|
|
32148
|
+
enrollments.set(userId, cloneEnrollment(previous));
|
|
32149
|
+
else
|
|
32150
|
+
enrollments.delete(userId);
|
|
32151
|
+
},
|
|
31831
32152
|
saveEnrollment: async (enrollment) => {
|
|
31832
32153
|
enrollments.set(enrollment.userId, cloneEnrollment(enrollment));
|
|
31833
32154
|
}
|
|
@@ -31840,12 +32161,16 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
31840
32161
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
31841
32162
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
31842
32163
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
32164
|
+
sms_challenge_id: text("sms_challenge_id"),
|
|
32165
|
+
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
31843
32166
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
31844
32167
|
sms_pending_code_expires_at_ms: bigint("sms_pending_code_expires_at_ms", {
|
|
31845
32168
|
mode: "number"
|
|
31846
32169
|
}),
|
|
31847
32170
|
sms_pending_code_hash: text("sms_pending_code_hash"),
|
|
32171
|
+
sms_pending_purpose: text("sms_pending_purpose").$type(),
|
|
31848
32172
|
sms_phone: varchar("sms_phone", { length: PHONE_LENGTH }),
|
|
32173
|
+
sms_provider_reference: text("sms_provider_reference"),
|
|
31849
32174
|
sms_verified: boolean("sms_verified").notNull().default(false),
|
|
31850
32175
|
totp_failed_attempts: smallint("totp_failed_attempts").notNull().default(0),
|
|
31851
32176
|
totp_secret_ciphertext: text("totp_secret_ciphertext"),
|
|
@@ -31857,10 +32182,14 @@ var toEnrollment = (row) => ({
|
|
|
31857
32182
|
backupCodeHashes: row.backup_code_hashes,
|
|
31858
32183
|
createdAt: row.created_at_ms,
|
|
31859
32184
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
32185
|
+
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
32186
|
+
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
31860
32187
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
31861
32188
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
31862
32189
|
smsPendingCodeHash: row.sms_pending_code_hash ?? undefined,
|
|
32190
|
+
smsPendingPurpose: row.sms_pending_purpose ?? undefined,
|
|
31863
32191
|
smsPhone: row.sms_phone ?? undefined,
|
|
32192
|
+
smsProviderReference: row.sms_provider_reference ?? undefined,
|
|
31864
32193
|
smsVerified: row.sms_verified,
|
|
31865
32194
|
totpFailedAttempts: row.totp_failed_attempts,
|
|
31866
32195
|
totpSecretCiphertext: row.totp_secret_ciphertext ?? undefined,
|
|
@@ -31869,40 +32198,118 @@ var toEnrollment = (row) => ({
|
|
|
31869
32198
|
userId: row.user_id
|
|
31870
32199
|
});
|
|
31871
32200
|
var createNeonMfaStore = (databaseUrl) => createPostgresMfaStore(createNeonDatabase(databaseUrl));
|
|
31872
|
-
var createPostgresMfaStore = (db) =>
|
|
31873
|
-
|
|
31874
|
-
|
|
31875
|
-
|
|
31876
|
-
|
|
31877
|
-
|
|
31878
|
-
|
|
31879
|
-
|
|
31880
|
-
|
|
31881
|
-
|
|
31882
|
-
|
|
31883
|
-
|
|
31884
|
-
|
|
31885
|
-
|
|
31886
|
-
|
|
31887
|
-
|
|
31888
|
-
|
|
31889
|
-
|
|
31890
|
-
|
|
31891
|
-
|
|
31892
|
-
|
|
31893
|
-
|
|
31894
|
-
|
|
31895
|
-
|
|
31896
|
-
|
|
31897
|
-
|
|
31898
|
-
|
|
31899
|
-
|
|
31900
|
-
|
|
31901
|
-
|
|
31902
|
-
|
|
31903
|
-
|
|
31904
|
-
|
|
31905
|
-
|
|
32201
|
+
var createPostgresMfaStore = (db) => {
|
|
32202
|
+
const toValues = (enrollment) => ({
|
|
32203
|
+
backup_code_hashes: enrollment.backupCodeHashes,
|
|
32204
|
+
created_at_ms: enrollment.createdAt,
|
|
32205
|
+
last_used_at_ms: enrollment.lastUsedAt ?? null,
|
|
32206
|
+
sms_challenge_id: enrollment.smsChallengeId ?? null,
|
|
32207
|
+
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
32208
|
+
sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
|
|
32209
|
+
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
32210
|
+
sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
|
|
32211
|
+
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
32212
|
+
sms_phone: enrollment.smsPhone ?? null,
|
|
32213
|
+
sms_provider_reference: enrollment.smsProviderReference ?? null,
|
|
32214
|
+
sms_verified: enrollment.smsVerified,
|
|
32215
|
+
totp_failed_attempts: enrollment.totpFailedAttempts ?? 0,
|
|
32216
|
+
totp_secret_ciphertext: enrollment.totpSecretCiphertext ?? null,
|
|
32217
|
+
totp_verified: enrollment.totpVerified,
|
|
32218
|
+
updated_at_ms: enrollment.updatedAt,
|
|
32219
|
+
user_id: enrollment.userId
|
|
32220
|
+
});
|
|
32221
|
+
return {
|
|
32222
|
+
claimSmsChallenge: async ({
|
|
32223
|
+
challengeId,
|
|
32224
|
+
cooldownCutoff,
|
|
32225
|
+
enrollment
|
|
32226
|
+
}) => {
|
|
32227
|
+
const values = toValues({
|
|
32228
|
+
...enrollment,
|
|
32229
|
+
smsChallengeId: challengeId
|
|
32230
|
+
});
|
|
32231
|
+
const rows = await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
32232
|
+
set: {
|
|
32233
|
+
sms_challenge_id: challengeId,
|
|
32234
|
+
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
32235
|
+
sms_failed_attempts: 0,
|
|
32236
|
+
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
32237
|
+
sms_pending_code_hash: null,
|
|
32238
|
+
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
32239
|
+
sms_phone: enrollment.smsPhone ?? null,
|
|
32240
|
+
sms_provider_reference: null,
|
|
32241
|
+
sms_verified: enrollment.smsVerified,
|
|
32242
|
+
updated_at_ms: enrollment.updatedAt
|
|
32243
|
+
},
|
|
32244
|
+
setWhere: or(isNull(mfaEnrollmentsTable.sms_code_sent_at_ms), lte(mfaEnrollmentsTable.sms_code_sent_at_ms, cooldownCutoff)),
|
|
32245
|
+
target: mfaEnrollmentsTable.user_id
|
|
32246
|
+
}).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
32247
|
+
return rows.length === 1;
|
|
32248
|
+
},
|
|
32249
|
+
completeSmsChallenge: async ({
|
|
32250
|
+
challengeId,
|
|
32251
|
+
lastUsedAt,
|
|
32252
|
+
smsVerified,
|
|
32253
|
+
userId
|
|
32254
|
+
}) => {
|
|
32255
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
32256
|
+
last_used_at_ms: lastUsedAt,
|
|
32257
|
+
sms_challenge_id: null,
|
|
32258
|
+
sms_failed_attempts: 0,
|
|
32259
|
+
sms_pending_code_expires_at_ms: null,
|
|
32260
|
+
sms_pending_code_hash: null,
|
|
32261
|
+
sms_pending_purpose: null,
|
|
32262
|
+
sms_provider_reference: null,
|
|
32263
|
+
sms_verified: smsVerified,
|
|
32264
|
+
updated_at_ms: Date.now()
|
|
32265
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
32266
|
+
return rows.length === 1;
|
|
32267
|
+
},
|
|
32268
|
+
finalizeSmsChallenge: async (input) => {
|
|
32269
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
32270
|
+
sms_pending_code_expires_at_ms: input.expiresAt,
|
|
32271
|
+
sms_pending_code_hash: input.hash ?? null,
|
|
32272
|
+
sms_provider_reference: input.providerReference ?? null,
|
|
32273
|
+
updated_at_ms: Date.now()
|
|
32274
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, input.userId), eq(mfaEnrollmentsTable.sms_challenge_id, input.challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
32275
|
+
return rows.length === 1;
|
|
32276
|
+
},
|
|
32277
|
+
getEnrollment: async (userId) => {
|
|
32278
|
+
const [row] = await db.select().from(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId)).limit(1);
|
|
32279
|
+
return row ? toEnrollment(row) : undefined;
|
|
32280
|
+
},
|
|
32281
|
+
listEnrollments: async () => {
|
|
32282
|
+
const rows = await db.select().from(mfaEnrollmentsTable);
|
|
32283
|
+
return rows.map(toEnrollment);
|
|
32284
|
+
},
|
|
32285
|
+
recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
|
|
32286
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
32287
|
+
sms_failed_attempts: sql`${mfaEnrollmentsTable.sms_failed_attempts} + 1`,
|
|
32288
|
+
updated_at_ms: Date.now()
|
|
32289
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId), lte(mfaEnrollmentsTable.sms_failed_attempts, maxAttempts - 1))).returning({
|
|
32290
|
+
attempts: mfaEnrollmentsTable.sms_failed_attempts
|
|
32291
|
+
});
|
|
32292
|
+
return rows[0]?.attempts;
|
|
32293
|
+
},
|
|
32294
|
+
removeEnrollment: async (userId) => {
|
|
32295
|
+
await db.delete(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId));
|
|
32296
|
+
},
|
|
32297
|
+
rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
|
|
32298
|
+
if (previous) {
|
|
32299
|
+
await db.update(mfaEnrollmentsTable).set(toValues(previous)).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
|
|
32300
|
+
return;
|
|
32301
|
+
}
|
|
32302
|
+
await db.delete(mfaEnrollmentsTable).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
|
|
32303
|
+
},
|
|
32304
|
+
saveEnrollment: async (enrollment) => {
|
|
32305
|
+
const values = toValues(enrollment);
|
|
32306
|
+
await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
32307
|
+
set: values,
|
|
32308
|
+
target: mfaEnrollmentsTable.user_id
|
|
32309
|
+
});
|
|
32310
|
+
}
|
|
32311
|
+
};
|
|
32312
|
+
};
|
|
31906
32313
|
// src/audit/types.ts
|
|
31907
32314
|
var AUDIT_EVENT_TYPES = [
|
|
31908
32315
|
"account_deleted",
|
|
@@ -36238,6 +36645,19 @@ var mfaTotpLockoutMigration = {
|
|
|
36238
36645
|
id: "0003_totp_lockout",
|
|
36239
36646
|
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "totp_failed_attempts" smallint NOT NULL DEFAULT 0;'
|
|
36240
36647
|
};
|
|
36648
|
+
var mfaSmsDeliveryPolicyMigration = {
|
|
36649
|
+
id: "0004_sms_delivery_policy",
|
|
36650
|
+
sql: [
|
|
36651
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_code_sent_at_ms" bigint;',
|
|
36652
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_pending_purpose" text;',
|
|
36653
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_provider_reference" text;'
|
|
36654
|
+
].join(`
|
|
36655
|
+
`)
|
|
36656
|
+
};
|
|
36657
|
+
var mfaSmsAtomicChallengeMigration = {
|
|
36658
|
+
id: "0005_sms_atomic_challenge",
|
|
36659
|
+
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_challenge_id" text;'
|
|
36660
|
+
};
|
|
36241
36661
|
var oidcResourceAudienceMigration = {
|
|
36242
36662
|
id: "0002_resource_audience",
|
|
36243
36663
|
sql: [
|
|
@@ -36292,7 +36712,9 @@ var blockMigrations = {
|
|
|
36292
36712
|
migrations: [
|
|
36293
36713
|
...initMigration("mfa", [mfaEnrollmentsTable]).migrations,
|
|
36294
36714
|
mfaSmsColumnsMigration,
|
|
36295
|
-
mfaTotpLockoutMigration
|
|
36715
|
+
mfaTotpLockoutMigration,
|
|
36716
|
+
mfaSmsDeliveryPolicyMigration,
|
|
36717
|
+
mfaSmsAtomicChallengeMigration
|
|
36296
36718
|
]
|
|
36297
36719
|
},
|
|
36298
36720
|
oidc: {
|
|
@@ -36805,6 +37227,7 @@ var buildAuthApplications = async (configuration) => {
|
|
|
36805
37227
|
credentials,
|
|
36806
37228
|
customProviders,
|
|
36807
37229
|
mfa,
|
|
37230
|
+
verificationProvider,
|
|
36808
37231
|
passwordless,
|
|
36809
37232
|
lockout,
|
|
36810
37233
|
sessions,
|
|
@@ -37037,7 +37460,8 @@ var buildAuthApplications = async (configuration) => {
|
|
|
37037
37460
|
auditedMfa ? mfaRoutes({
|
|
37038
37461
|
...auditedMfa,
|
|
37039
37462
|
authSessionStore,
|
|
37040
|
-
cookieSecure: resolvedCookieSecure
|
|
37463
|
+
cookieSecure: resolvedCookieSecure,
|
|
37464
|
+
verificationProvider
|
|
37041
37465
|
}) : new Elysia45,
|
|
37042
37466
|
passwordless ? passwordlessRoutes({
|
|
37043
37467
|
...passwordless,
|
|
@@ -37564,6 +37988,7 @@ export {
|
|
|
37564
37988
|
accessTokensTable,
|
|
37565
37989
|
acceptInvitation,
|
|
37566
37990
|
WEBAUTHN_CHALLENGE_COOKIE,
|
|
37991
|
+
VerificationProviderError,
|
|
37567
37992
|
STATUS_LIST_TYP,
|
|
37568
37993
|
STATUS_LIST_SUB_TYP,
|
|
37569
37994
|
REQUEST_URI_PREFIX,
|
|
@@ -37581,6 +38006,7 @@ export {
|
|
|
37581
38006
|
DEFAULT_STATUS_ROUTE,
|
|
37582
38007
|
DEFAULT_SSO_SESSION_TTL_MS,
|
|
37583
38008
|
DEFAULT_SSO_ROUTE,
|
|
38009
|
+
DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
37584
38010
|
DEFAULT_SMS_MAX_ATTEMPTS,
|
|
37585
38011
|
DEFAULT_SMS_CODE_TTL_MS,
|
|
37586
38012
|
DEFAULT_SMS_CODE_LENGTH,
|
|
@@ -37598,6 +38024,7 @@ export {
|
|
|
37598
38024
|
DEFAULT_ORGANIZATIONS_ROUTE,
|
|
37599
38025
|
DEFAULT_OIDC_ROUTE,
|
|
37600
38026
|
DEFAULT_MFA_SESSION_TTL_MS,
|
|
38027
|
+
DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
37601
38028
|
DEFAULT_MFA_ISSUER,
|
|
37602
38029
|
DEFAULT_MAGIC_LINK_TTL_MS,
|
|
37603
38030
|
DEFAULT_INVITATION_TTL_MS,
|
|
@@ -37613,5 +38040,5 @@ export {
|
|
|
37613
38040
|
AGENT_CLAIM_GRANT_TYPE
|
|
37614
38041
|
};
|
|
37615
38042
|
|
|
37616
|
-
//# debugId=
|
|
38043
|
+
//# debugId=FC3F27A521162E4064756E2164756E21
|
|
37617
38044
|
//# sourceMappingURL=index.js.map
|