@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/server.js
CHANGED
|
@@ -6844,11 +6844,13 @@ var generateBackupCodes = async (count) => {
|
|
|
6844
6844
|
init_constants();
|
|
6845
6845
|
var DEFAULT_BACKUP_CODE_COUNT = 10;
|
|
6846
6846
|
var DEFAULT_MFA_ISSUER = "AbsoluteAuth";
|
|
6847
|
+
var DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS = 5 * SECONDS_IN_A_MINUTE * MILLISECONDS_IN_A_SECOND;
|
|
6847
6848
|
var DEFAULT_MFA_SESSION_TTL_MS = MILLISECONDS_IN_A_DAY;
|
|
6848
6849
|
var DEFAULT_SMS_CODE_LENGTH = 6;
|
|
6849
6850
|
var SMS_CODE_TTL_MINUTES = 5;
|
|
6850
6851
|
var DEFAULT_SMS_CODE_TTL_MS = SMS_CODE_TTL_MINUTES * SECONDS_IN_A_MINUTE * MILLISECONDS_IN_A_SECOND;
|
|
6851
6852
|
var DEFAULT_SMS_MAX_ATTEMPTS = 3;
|
|
6853
|
+
var DEFAULT_SMS_RESEND_COOLDOWN_MS = 30 * MILLISECONDS_IN_A_SECOND;
|
|
6852
6854
|
var DEFAULT_TOTP_MAX_ATTEMPTS = 5;
|
|
6853
6855
|
|
|
6854
6856
|
// src/mfa/secret.ts
|
|
@@ -6859,6 +6861,23 @@ var encryptTotpSecret = (secret, encryptionKey) => encryptionKey ? encryptSecret
|
|
|
6859
6861
|
// src/mfa/sms.ts
|
|
6860
6862
|
init_crypto();
|
|
6861
6863
|
import { Elysia as Elysia17, t as t11 } from "elysia";
|
|
6864
|
+
|
|
6865
|
+
// src/verification/types.ts
|
|
6866
|
+
class VerificationProviderError extends Error {
|
|
6867
|
+
kind;
|
|
6868
|
+
provider;
|
|
6869
|
+
constructor(input) {
|
|
6870
|
+
super(input.message, { cause: input.cause });
|
|
6871
|
+
this.name = "VerificationProviderError";
|
|
6872
|
+
this.kind = input.kind;
|
|
6873
|
+
this.provider = input.provider;
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
// src/mfa/recentAuth.ts
|
|
6878
|
+
var hasRecentAuthentication = (session, maxAgeMs, now = Date.now()) => session.authenticatedAt !== undefined && now >= session.authenticatedAt && now - session.authenticatedAt <= maxAgeMs;
|
|
6879
|
+
|
|
6880
|
+
// src/mfa/sms.ts
|
|
6862
6881
|
var DECIMAL_RADIX2 = 10;
|
|
6863
6882
|
var MASK_VISIBLE_DIGITS = 4;
|
|
6864
6883
|
var E164_PATTERN = /^\+[1-9]\d{7,14}$/u;
|
|
@@ -6873,28 +6892,147 @@ var issueSmsCode = async (codeLength, ttlMs) => {
|
|
|
6873
6892
|
const expiresAt = Date.now() + ttlMs;
|
|
6874
6893
|
return { code, expiresAt, hash };
|
|
6875
6894
|
};
|
|
6895
|
+
var startProviderSmsChallenge = async (input) => {
|
|
6896
|
+
let started;
|
|
6897
|
+
try {
|
|
6898
|
+
started = await input.verificationProvider.start({
|
|
6899
|
+
channel: "sms",
|
|
6900
|
+
purpose: input.purpose,
|
|
6901
|
+
subject: input.userId,
|
|
6902
|
+
to: input.phone
|
|
6903
|
+
});
|
|
6904
|
+
} catch (error) {
|
|
6905
|
+
await input.mfaStore.rollbackSmsChallenge({
|
|
6906
|
+
challengeId: input.challengeId,
|
|
6907
|
+
previous: input.previousEnrollment,
|
|
6908
|
+
userId: input.userId
|
|
6909
|
+
});
|
|
6910
|
+
throw error;
|
|
6911
|
+
}
|
|
6912
|
+
const finalized = await input.mfaStore.finalizeSmsChallenge({
|
|
6913
|
+
challengeId: input.challengeId,
|
|
6914
|
+
expiresAt: started.expiresAt,
|
|
6915
|
+
providerReference: started.reference,
|
|
6916
|
+
userId: input.userId
|
|
6917
|
+
});
|
|
6918
|
+
if (finalized)
|
|
6919
|
+
return started;
|
|
6920
|
+
await input.verificationProvider.cancel({
|
|
6921
|
+
channel: "sms",
|
|
6922
|
+
purpose: input.purpose,
|
|
6923
|
+
reference: started.reference,
|
|
6924
|
+
subject: input.userId,
|
|
6925
|
+
to: input.phone
|
|
6926
|
+
}).catch(() => {
|
|
6927
|
+
return;
|
|
6928
|
+
});
|
|
6929
|
+
throw new SmsChallengeConflictError("SMS challenge claim was lost");
|
|
6930
|
+
};
|
|
6931
|
+
|
|
6932
|
+
class SmsChallengeConflictError extends Error {
|
|
6933
|
+
name = "SmsChallengeConflictError";
|
|
6934
|
+
}
|
|
6935
|
+
var checkWithVerificationProvider = async (provider, input) => {
|
|
6936
|
+
try {
|
|
6937
|
+
return { result: await provider.check(input) };
|
|
6938
|
+
} catch (error) {
|
|
6939
|
+
const mapped = mapVerificationProviderError(error);
|
|
6940
|
+
if (mapped === undefined)
|
|
6941
|
+
throw error;
|
|
6942
|
+
return { error: mapped };
|
|
6943
|
+
}
|
|
6944
|
+
};
|
|
6876
6945
|
var isE164Phone = (phone) => E164_PATTERN.test(phone);
|
|
6877
6946
|
var issueAndStoreSmsCode = async ({
|
|
6878
6947
|
codeLength,
|
|
6879
6948
|
enrollment,
|
|
6880
6949
|
mfaStore,
|
|
6881
6950
|
onSendSmsCode,
|
|
6882
|
-
|
|
6951
|
+
previousEnrollment,
|
|
6952
|
+
purpose,
|
|
6953
|
+
resendCooldownMs,
|
|
6954
|
+
verificationProvider,
|
|
6955
|
+
ttlMs,
|
|
6956
|
+
userId
|
|
6883
6957
|
}) => {
|
|
6884
6958
|
const phone = enrollment.smsPhone;
|
|
6885
6959
|
if (phone === undefined)
|
|
6886
6960
|
return;
|
|
6961
|
+
const challengeId = crypto.randomUUID();
|
|
6962
|
+
const now = Date.now();
|
|
6963
|
+
const claimed = await mfaStore.claimSmsChallenge({
|
|
6964
|
+
challengeId,
|
|
6965
|
+
cooldownCutoff: now - resendCooldownMs,
|
|
6966
|
+
enrollment: {
|
|
6967
|
+
...enrollment,
|
|
6968
|
+
smsChallengeId: challengeId,
|
|
6969
|
+
smsCodeSentAt: now,
|
|
6970
|
+
smsFailedAttempts: 0,
|
|
6971
|
+
smsPendingCodeExpiresAt: now + ttlMs,
|
|
6972
|
+
smsPendingCodeHash: undefined,
|
|
6973
|
+
smsPendingPurpose: purpose,
|
|
6974
|
+
smsProviderReference: undefined,
|
|
6975
|
+
updatedAt: now
|
|
6976
|
+
}
|
|
6977
|
+
});
|
|
6978
|
+
if (!claimed)
|
|
6979
|
+
throw new SmsChallengeConflictError("SMS resend cooldown active");
|
|
6980
|
+
if (verificationProvider !== undefined) {
|
|
6981
|
+
const started = await startProviderSmsChallenge({
|
|
6982
|
+
challengeId,
|
|
6983
|
+
mfaStore,
|
|
6984
|
+
phone,
|
|
6985
|
+
previousEnrollment,
|
|
6986
|
+
purpose,
|
|
6987
|
+
userId,
|
|
6988
|
+
verificationProvider
|
|
6989
|
+
});
|
|
6990
|
+
return started.expiresAt;
|
|
6991
|
+
}
|
|
6887
6992
|
const { code, expiresAt, hash } = await issueSmsCode(codeLength, ttlMs);
|
|
6888
|
-
await mfaStore.
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
updatedAt: Date.now()
|
|
6993
|
+
const finalized = await mfaStore.finalizeSmsChallenge({
|
|
6994
|
+
challengeId,
|
|
6995
|
+
expiresAt,
|
|
6996
|
+
hash,
|
|
6997
|
+
userId
|
|
6894
6998
|
});
|
|
6895
|
-
|
|
6999
|
+
if (!finalized)
|
|
7000
|
+
throw new SmsChallengeConflictError("SMS challenge claim was lost");
|
|
7001
|
+
try {
|
|
7002
|
+
await onSendSmsCode?.({ code, expiresAt, phone, purpose, userId });
|
|
7003
|
+
} catch (error) {
|
|
7004
|
+
await mfaStore.rollbackSmsChallenge({
|
|
7005
|
+
challengeId,
|
|
7006
|
+
previous: previousEnrollment,
|
|
7007
|
+
userId
|
|
7008
|
+
});
|
|
7009
|
+
throw error;
|
|
7010
|
+
}
|
|
6896
7011
|
return expiresAt;
|
|
6897
7012
|
};
|
|
7013
|
+
var mapVerificationProviderError = (error) => {
|
|
7014
|
+
if (error instanceof SmsChallengeConflictError) {
|
|
7015
|
+
return { message: error.message, status: "Too Many Requests" };
|
|
7016
|
+
}
|
|
7017
|
+
if (!(error instanceof VerificationProviderError))
|
|
7018
|
+
return;
|
|
7019
|
+
if (error.kind === "rate_limited") {
|
|
7020
|
+
return {
|
|
7021
|
+
message: "Verification provider rate limit reached",
|
|
7022
|
+
status: "Too Many Requests"
|
|
7023
|
+
};
|
|
7024
|
+
}
|
|
7025
|
+
if (error.kind === "invalid_destination") {
|
|
7026
|
+
return {
|
|
7027
|
+
message: "Phone number cannot receive verification codes",
|
|
7028
|
+
status: "Bad Request"
|
|
7029
|
+
};
|
|
7030
|
+
}
|
|
7031
|
+
return {
|
|
7032
|
+
message: "Verification provider unavailable",
|
|
7033
|
+
status: "Service Unavailable"
|
|
7034
|
+
};
|
|
7035
|
+
};
|
|
6898
7036
|
var maskPhone = (phone) => {
|
|
6899
7037
|
const visible = phone.slice(-MASK_VISIBLE_DIGITS);
|
|
6900
7038
|
const maskedLength = Math.max(phone.length - MASK_VISIBLE_DIGITS, 0);
|
|
@@ -6904,11 +7042,14 @@ var mfaSmsRoutes = ({
|
|
|
6904
7042
|
authSessionStore,
|
|
6905
7043
|
getUserId,
|
|
6906
7044
|
mfaStore,
|
|
7045
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6907
7046
|
onMfaEnrolled,
|
|
6908
7047
|
onSendSmsCode,
|
|
7048
|
+
verificationProvider,
|
|
6909
7049
|
smsCodeLength = DEFAULT_SMS_CODE_LENGTH,
|
|
6910
7050
|
smsCodeTtlMs = DEFAULT_SMS_CODE_TTL_MS,
|
|
6911
7051
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
7052
|
+
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
6912
7053
|
smsSetupRoute = "/auth/mfa/sms/setup",
|
|
6913
7054
|
smsVerifyRoute = "/auth/mfa/sms/verify"
|
|
6914
7055
|
}) => new Elysia17().use(sessionStore()).post(smsSetupRoute, async ({
|
|
@@ -6917,7 +7058,7 @@ var mfaSmsRoutes = ({
|
|
|
6917
7058
|
status,
|
|
6918
7059
|
store: { session }
|
|
6919
7060
|
}) => {
|
|
6920
|
-
if (onSendSmsCode === undefined) {
|
|
7061
|
+
if (onSendSmsCode === undefined && verificationProvider === undefined) {
|
|
6921
7062
|
return status("Not Implemented", "SMS MFA is not configured");
|
|
6922
7063
|
}
|
|
6923
7064
|
const userSession = await loadSessionFromSource({
|
|
@@ -6928,27 +7069,49 @@ var mfaSmsRoutes = ({
|
|
|
6928
7069
|
if (!userSession) {
|
|
6929
7070
|
return status("Unauthorized", "Authentication required");
|
|
6930
7071
|
}
|
|
7072
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7073
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7074
|
+
}
|
|
6931
7075
|
if (!isE164Phone(phone)) {
|
|
6932
7076
|
return status("Bad Request", "Phone must be in E.164 format");
|
|
6933
7077
|
}
|
|
6934
7078
|
const userId = getUserId(userSession.user);
|
|
6935
7079
|
const existing = await mfaStore.getEnrollment(userId);
|
|
6936
7080
|
const now = Date.now();
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
7081
|
+
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
7082
|
+
return status("Too Many Requests", "SMS resend cooldown active");
|
|
7083
|
+
}
|
|
7084
|
+
try {
|
|
7085
|
+
await issueAndStoreSmsCode({
|
|
7086
|
+
codeLength: smsCodeLength,
|
|
7087
|
+
enrollment: {
|
|
7088
|
+
backupCodeHashes: existing?.backupCodeHashes ?? [],
|
|
7089
|
+
createdAt: existing?.createdAt ?? now,
|
|
7090
|
+
lastUsedAt: existing?.lastUsedAt,
|
|
7091
|
+
smsFailedAttempts: 0,
|
|
7092
|
+
smsPhone: phone,
|
|
7093
|
+
smsVerified: false,
|
|
7094
|
+
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
7095
|
+
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
7096
|
+
totpVerified: existing?.totpVerified ?? false,
|
|
7097
|
+
updatedAt: now,
|
|
7098
|
+
userId
|
|
7099
|
+
},
|
|
7100
|
+
mfaStore,
|
|
7101
|
+
onSendSmsCode,
|
|
7102
|
+
previousEnrollment: existing,
|
|
7103
|
+
purpose: "mfa_enrollment",
|
|
7104
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
7105
|
+
ttlMs: smsCodeTtlMs,
|
|
7106
|
+
userId,
|
|
7107
|
+
verificationProvider
|
|
7108
|
+
});
|
|
7109
|
+
} catch (error) {
|
|
7110
|
+
const mapped = mapVerificationProviderError(error);
|
|
7111
|
+
if (mapped === undefined)
|
|
7112
|
+
throw error;
|
|
7113
|
+
return status(mapped.status, mapped.message);
|
|
7114
|
+
}
|
|
6952
7115
|
return status("OK", { phone: maskPhone(phone) });
|
|
6953
7116
|
}, {
|
|
6954
7117
|
body: t11.Object({ phone: t11.String() }),
|
|
@@ -6967,34 +7130,65 @@ var mfaSmsRoutes = ({
|
|
|
6967
7130
|
if (!userSession) {
|
|
6968
7131
|
return status("Unauthorized", "Authentication required");
|
|
6969
7132
|
}
|
|
7133
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7134
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7135
|
+
}
|
|
6970
7136
|
const userId = getUserId(userSession.user);
|
|
6971
7137
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
6972
|
-
if (!enrollment?.
|
|
7138
|
+
if (!enrollment?.smsPhone) {
|
|
7139
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
7140
|
+
}
|
|
7141
|
+
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
7142
|
+
const challengeId = enrollment.smsChallengeId;
|
|
7143
|
+
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
7144
|
+
const providerReference = enrollment.smsProviderReference;
|
|
7145
|
+
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
6973
7146
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
6974
7147
|
}
|
|
6975
|
-
if (
|
|
7148
|
+
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
7149
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
7150
|
+
}
|
|
7151
|
+
if (Date.now() > localCodeExpiresAt) {
|
|
6976
7152
|
return status("Bad Request", "SMS code expired");
|
|
6977
7153
|
}
|
|
6978
7154
|
if ((enrollment.smsFailedAttempts ?? 0) >= smsMaxAttempts) {
|
|
6979
7155
|
return status("Too Many Requests", "Too many attempts");
|
|
6980
7156
|
}
|
|
6981
|
-
|
|
7157
|
+
let providerResult;
|
|
7158
|
+
if (verificationProvider !== undefined) {
|
|
7159
|
+
if (providerReference === undefined) {
|
|
7160
|
+
return status("Bad Request", "No SMS enrollment in progress");
|
|
7161
|
+
}
|
|
7162
|
+
const checked = await checkWithVerificationProvider(verificationProvider, {
|
|
7163
|
+
channel: "sms",
|
|
7164
|
+
code,
|
|
7165
|
+
purpose: "mfa_enrollment",
|
|
7166
|
+
reference: providerReference,
|
|
7167
|
+
subject: userId,
|
|
7168
|
+
to: enrollment.smsPhone
|
|
7169
|
+
});
|
|
7170
|
+
if (checked.error !== undefined) {
|
|
7171
|
+
return status(checked.error.status, checked.error.message);
|
|
7172
|
+
}
|
|
7173
|
+
providerResult = checked.result;
|
|
7174
|
+
}
|
|
7175
|
+
const codeValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
6982
7176
|
if (!codeValid) {
|
|
6983
|
-
await mfaStore.
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
7177
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
7178
|
+
challengeId,
|
|
7179
|
+
maxAttempts: smsMaxAttempts,
|
|
7180
|
+
userId
|
|
6987
7181
|
});
|
|
6988
|
-
return status("Bad Request", "Invalid SMS code");
|
|
7182
|
+
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "Invalid SMS code");
|
|
6989
7183
|
}
|
|
6990
|
-
await mfaStore.
|
|
6991
|
-
|
|
6992
|
-
smsFailedAttempts: 0,
|
|
6993
|
-
smsPendingCodeExpiresAt: undefined,
|
|
6994
|
-
smsPendingCodeHash: undefined,
|
|
7184
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
7185
|
+
challengeId,
|
|
6995
7186
|
smsVerified: true,
|
|
6996
|
-
|
|
7187
|
+
userId
|
|
6997
7188
|
});
|
|
7189
|
+
if (!completed) {
|
|
7190
|
+
return status("Bad Request", "SMS challenge is no longer active");
|
|
7191
|
+
}
|
|
6998
7192
|
await onMfaEnrolled?.({ userId });
|
|
6999
7193
|
return status("OK", { status: "enrolled" });
|
|
7000
7194
|
}, {
|
|
@@ -7014,10 +7208,12 @@ var mfaChallenge = ({
|
|
|
7014
7208
|
onMfaChallengeError,
|
|
7015
7209
|
onMfaChallengeSuccess,
|
|
7016
7210
|
onSendSmsCode,
|
|
7211
|
+
verificationProvider,
|
|
7017
7212
|
sessionDurationMs = DEFAULT_MFA_SESSION_TTL_MS,
|
|
7018
7213
|
smsCodeLength = DEFAULT_SMS_CODE_LENGTH,
|
|
7019
7214
|
smsCodeTtlMs = DEFAULT_SMS_CODE_TTL_MS,
|
|
7020
7215
|
smsMaxAttempts = DEFAULT_SMS_MAX_ATTEMPTS,
|
|
7216
|
+
smsResendCooldownMs = DEFAULT_SMS_RESEND_COOLDOWN_MS,
|
|
7021
7217
|
totpMaxAttempts = DEFAULT_TOTP_MAX_ATTEMPTS
|
|
7022
7218
|
}) => new Elysia18().use(sessionStore()).post(challengeRoute, async ({
|
|
7023
7219
|
body: { action, code, factor },
|
|
@@ -7059,27 +7255,51 @@ var mfaChallenge = ({
|
|
|
7059
7255
|
await onMfaChallengeSuccess?.({ user, userSessionId });
|
|
7060
7256
|
return status("OK", { status: "authenticated" });
|
|
7061
7257
|
};
|
|
7062
|
-
const
|
|
7063
|
-
if (
|
|
7064
|
-
return status("
|
|
7258
|
+
const sendSmsChallenge = async () => {
|
|
7259
|
+
if (enrollment.smsCodeSentAt !== undefined && Date.now() - enrollment.smsCodeSentAt < smsResendCooldownMs) {
|
|
7260
|
+
return status("Too Many Requests", "SMS resend cooldown active");
|
|
7065
7261
|
}
|
|
7066
|
-
|
|
7262
|
+
try {
|
|
7067
7263
|
await issueAndStoreSmsCode({
|
|
7068
7264
|
codeLength: smsCodeLength,
|
|
7069
7265
|
enrollment,
|
|
7070
7266
|
mfaStore,
|
|
7071
7267
|
onSendSmsCode,
|
|
7072
|
-
|
|
7268
|
+
previousEnrollment: enrollment,
|
|
7269
|
+
purpose: "mfa_challenge",
|
|
7270
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
7271
|
+
ttlMs: smsCodeTtlMs,
|
|
7272
|
+
userId: getUserId(user),
|
|
7273
|
+
verificationProvider
|
|
7073
7274
|
});
|
|
7074
|
-
|
|
7275
|
+
} catch (error) {
|
|
7276
|
+
const mapped = mapVerificationProviderError(error);
|
|
7277
|
+
if (mapped === undefined)
|
|
7278
|
+
throw error;
|
|
7279
|
+
return status(mapped.status, mapped.message);
|
|
7075
7280
|
}
|
|
7281
|
+
return status("OK", { status: "sent" });
|
|
7282
|
+
};
|
|
7283
|
+
const runSmsChallenge = async () => {
|
|
7284
|
+
if (!enrollment.smsVerified || !enrollment.smsPhone) {
|
|
7285
|
+
return status("Unauthorized", "No MFA challenge in progress");
|
|
7286
|
+
}
|
|
7287
|
+
if (action === "send")
|
|
7288
|
+
return sendSmsChallenge();
|
|
7076
7289
|
if (code === undefined) {
|
|
7077
7290
|
return status("Bad Request", "SMS code required");
|
|
7078
7291
|
}
|
|
7079
|
-
|
|
7292
|
+
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
7293
|
+
const challengeId = enrollment.smsChallengeId;
|
|
7294
|
+
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
7295
|
+
const providerReference = enrollment.smsProviderReference;
|
|
7296
|
+
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
7297
|
+
return status("Bad Request", "No SMS code in progress");
|
|
7298
|
+
}
|
|
7299
|
+
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
7080
7300
|
return status("Bad Request", "No SMS code in progress");
|
|
7081
7301
|
}
|
|
7082
|
-
if (Date.now() >
|
|
7302
|
+
if (Date.now() > localCodeExpiresAt) {
|
|
7083
7303
|
return status("Unauthorized", "SMS code expired");
|
|
7084
7304
|
}
|
|
7085
7305
|
if ((enrollment.smsFailedAttempts ?? 0) >= smsMaxAttempts) {
|
|
@@ -7089,27 +7309,46 @@ var mfaChallenge = ({
|
|
|
7089
7309
|
});
|
|
7090
7310
|
return status("Unauthorized", "Too many attempts");
|
|
7091
7311
|
}
|
|
7092
|
-
|
|
7312
|
+
let providerResult;
|
|
7313
|
+
if (verificationProvider !== undefined) {
|
|
7314
|
+
if (providerReference === undefined) {
|
|
7315
|
+
return status("Bad Request", "No SMS code in progress");
|
|
7316
|
+
}
|
|
7317
|
+
const checked = await checkWithVerificationProvider(verificationProvider, {
|
|
7318
|
+
channel: "sms",
|
|
7319
|
+
code,
|
|
7320
|
+
purpose: "mfa_challenge",
|
|
7321
|
+
reference: providerReference,
|
|
7322
|
+
subject: getUserId(user),
|
|
7323
|
+
to: enrollment.smsPhone
|
|
7324
|
+
});
|
|
7325
|
+
if (checked.error !== undefined) {
|
|
7326
|
+
return status(checked.error.status, checked.error.message);
|
|
7327
|
+
}
|
|
7328
|
+
providerResult = checked.result;
|
|
7329
|
+
}
|
|
7330
|
+
const smsValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
7093
7331
|
if (!smsValid) {
|
|
7094
|
-
await mfaStore.
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7332
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
7333
|
+
challengeId,
|
|
7334
|
+
maxAttempts: smsMaxAttempts,
|
|
7335
|
+
userId: getUserId(user)
|
|
7098
7336
|
});
|
|
7099
7337
|
await onMfaChallengeError?.({
|
|
7100
7338
|
error: new Error("invalid_mfa_code"),
|
|
7101
7339
|
userId: getUserId(user)
|
|
7102
7340
|
});
|
|
7103
|
-
return status("Unauthorized", "Invalid MFA code");
|
|
7341
|
+
return status(providerResult?.status === "max_attempts_reached" || attempts === undefined ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "Invalid MFA code");
|
|
7104
7342
|
}
|
|
7105
|
-
await mfaStore.
|
|
7106
|
-
|
|
7343
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
7344
|
+
challengeId,
|
|
7107
7345
|
lastUsedAt: Date.now(),
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
smsPendingCodeHash: undefined,
|
|
7111
|
-
updatedAt: Date.now()
|
|
7346
|
+
smsVerified: true,
|
|
7347
|
+
userId: getUserId(user)
|
|
7112
7348
|
});
|
|
7349
|
+
if (!completed) {
|
|
7350
|
+
return status("Unauthorized", "SMS challenge is no longer active");
|
|
7351
|
+
}
|
|
7113
7352
|
return promote();
|
|
7114
7353
|
};
|
|
7115
7354
|
if (factor === "sms")
|
|
@@ -7171,6 +7410,7 @@ var mfaManagementRoutes = ({
|
|
|
7171
7410
|
authSessionStore,
|
|
7172
7411
|
getUserId,
|
|
7173
7412
|
managementRoute = "/auth/mfa",
|
|
7413
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
7174
7414
|
mfaStore
|
|
7175
7415
|
}) => new Elysia19().use(sessionStore()).get(managementRoute, async ({
|
|
7176
7416
|
cookie: { user_session_id },
|
|
@@ -7209,6 +7449,9 @@ var mfaManagementRoutes = ({
|
|
|
7209
7449
|
if (!userSession) {
|
|
7210
7450
|
return status("Unauthorized", "Authentication required");
|
|
7211
7451
|
}
|
|
7452
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7453
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7454
|
+
}
|
|
7212
7455
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
7213
7456
|
return status("OK", { status: "disabled" });
|
|
7214
7457
|
}, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) });
|
|
@@ -7222,6 +7465,7 @@ var mfaTotpRoutes = ({
|
|
|
7222
7465
|
encryptionKey,
|
|
7223
7466
|
getUserId,
|
|
7224
7467
|
issuer = DEFAULT_MFA_ISSUER,
|
|
7468
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
7225
7469
|
mfaStore,
|
|
7226
7470
|
onMfaEnrolled,
|
|
7227
7471
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
@@ -7239,6 +7483,9 @@ var mfaTotpRoutes = ({
|
|
|
7239
7483
|
if (!userSession) {
|
|
7240
7484
|
return status("Unauthorized", "Authentication required");
|
|
7241
7485
|
}
|
|
7486
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7487
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7488
|
+
}
|
|
7242
7489
|
const userId = getUserId(userSession.user);
|
|
7243
7490
|
const secret = generateTotpSecret();
|
|
7244
7491
|
const existing = await mfaStore.getEnrollment(userId);
|
|
@@ -7278,6 +7525,9 @@ var mfaTotpRoutes = ({
|
|
|
7278
7525
|
if (!userSession) {
|
|
7279
7526
|
return status("Unauthorized", "Authentication required");
|
|
7280
7527
|
}
|
|
7528
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7529
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7530
|
+
}
|
|
7281
7531
|
const userId = getUserId(userSession.user);
|
|
7282
7532
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7283
7533
|
if (!enrollment?.totpSecretCiphertext) {
|
|
@@ -32816,14 +33066,85 @@ var cloneEnrollment = (value) => ({
|
|
|
32816
33066
|
var createInMemoryMfaStore = () => {
|
|
32817
33067
|
const enrollments = new Map;
|
|
32818
33068
|
return {
|
|
33069
|
+
claimSmsChallenge: async ({
|
|
33070
|
+
challengeId,
|
|
33071
|
+
cooldownCutoff,
|
|
33072
|
+
enrollment
|
|
33073
|
+
}) => {
|
|
33074
|
+
const current = enrollments.get(enrollment.userId);
|
|
33075
|
+
if (current?.smsCodeSentAt !== undefined && current.smsCodeSentAt > cooldownCutoff) {
|
|
33076
|
+
return false;
|
|
33077
|
+
}
|
|
33078
|
+
enrollments.set(enrollment.userId, cloneEnrollment({
|
|
33079
|
+
...enrollment,
|
|
33080
|
+
smsChallengeId: challengeId
|
|
33081
|
+
}));
|
|
33082
|
+
return true;
|
|
33083
|
+
},
|
|
33084
|
+
completeSmsChallenge: async ({
|
|
33085
|
+
challengeId,
|
|
33086
|
+
lastUsedAt,
|
|
33087
|
+
smsVerified,
|
|
33088
|
+
userId
|
|
33089
|
+
}) => {
|
|
33090
|
+
const current = enrollments.get(userId);
|
|
33091
|
+
if (current?.smsChallengeId !== challengeId)
|
|
33092
|
+
return false;
|
|
33093
|
+
enrollments.set(userId, cloneEnrollment({
|
|
33094
|
+
...current,
|
|
33095
|
+
lastUsedAt: lastUsedAt ?? current.lastUsedAt,
|
|
33096
|
+
smsChallengeId: undefined,
|
|
33097
|
+
smsFailedAttempts: 0,
|
|
33098
|
+
smsPendingCodeExpiresAt: undefined,
|
|
33099
|
+
smsPendingCodeHash: undefined,
|
|
33100
|
+
smsPendingPurpose: undefined,
|
|
33101
|
+
smsProviderReference: undefined,
|
|
33102
|
+
smsVerified,
|
|
33103
|
+
updatedAt: Date.now()
|
|
33104
|
+
}));
|
|
33105
|
+
return true;
|
|
33106
|
+
},
|
|
33107
|
+
finalizeSmsChallenge: async (input) => {
|
|
33108
|
+
const current = enrollments.get(input.userId);
|
|
33109
|
+
if (current?.smsChallengeId !== input.challengeId)
|
|
33110
|
+
return false;
|
|
33111
|
+
enrollments.set(input.userId, cloneEnrollment({
|
|
33112
|
+
...current,
|
|
33113
|
+
smsPendingCodeExpiresAt: input.expiresAt,
|
|
33114
|
+
smsPendingCodeHash: input.hash,
|
|
33115
|
+
smsProviderReference: input.providerReference,
|
|
33116
|
+
updatedAt: Date.now()
|
|
33117
|
+
}));
|
|
33118
|
+
return true;
|
|
33119
|
+
},
|
|
32819
33120
|
getEnrollment: async (userId) => {
|
|
32820
33121
|
const enrollment = enrollments.get(userId);
|
|
32821
33122
|
return enrollment ? cloneEnrollment(enrollment) : undefined;
|
|
32822
33123
|
},
|
|
32823
33124
|
listEnrollments: async () => Array.from(enrollments.values()).map(cloneEnrollment),
|
|
33125
|
+
recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
|
|
33126
|
+
const current = enrollments.get(userId);
|
|
33127
|
+
if (current?.smsChallengeId !== challengeId || (current.smsFailedAttempts ?? 0) >= maxAttempts)
|
|
33128
|
+
return;
|
|
33129
|
+
const attempts = (current.smsFailedAttempts ?? 0) + 1;
|
|
33130
|
+
enrollments.set(userId, cloneEnrollment({
|
|
33131
|
+
...current,
|
|
33132
|
+
smsFailedAttempts: attempts,
|
|
33133
|
+
updatedAt: Date.now()
|
|
33134
|
+
}));
|
|
33135
|
+
return attempts;
|
|
33136
|
+
},
|
|
32824
33137
|
removeEnrollment: async (userId) => {
|
|
32825
33138
|
enrollments.delete(userId);
|
|
32826
33139
|
},
|
|
33140
|
+
rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
|
|
33141
|
+
if (enrollments.get(userId)?.smsChallengeId !== challengeId)
|
|
33142
|
+
return;
|
|
33143
|
+
if (previous)
|
|
33144
|
+
enrollments.set(userId, cloneEnrollment(previous));
|
|
33145
|
+
else
|
|
33146
|
+
enrollments.delete(userId);
|
|
33147
|
+
},
|
|
32827
33148
|
saveEnrollment: async (enrollment) => {
|
|
32828
33149
|
enrollments.set(enrollment.userId, cloneEnrollment(enrollment));
|
|
32829
33150
|
}
|
|
@@ -32836,12 +33157,16 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
32836
33157
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
32837
33158
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
32838
33159
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
33160
|
+
sms_challenge_id: text("sms_challenge_id"),
|
|
33161
|
+
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
32839
33162
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
32840
33163
|
sms_pending_code_expires_at_ms: bigint("sms_pending_code_expires_at_ms", {
|
|
32841
33164
|
mode: "number"
|
|
32842
33165
|
}),
|
|
32843
33166
|
sms_pending_code_hash: text("sms_pending_code_hash"),
|
|
33167
|
+
sms_pending_purpose: text("sms_pending_purpose").$type(),
|
|
32844
33168
|
sms_phone: varchar("sms_phone", { length: PHONE_LENGTH }),
|
|
33169
|
+
sms_provider_reference: text("sms_provider_reference"),
|
|
32845
33170
|
sms_verified: boolean("sms_verified").notNull().default(false),
|
|
32846
33171
|
totp_failed_attempts: smallint("totp_failed_attempts").notNull().default(0),
|
|
32847
33172
|
totp_secret_ciphertext: text("totp_secret_ciphertext"),
|
|
@@ -32853,10 +33178,14 @@ var toEnrollment = (row) => ({
|
|
|
32853
33178
|
backupCodeHashes: row.backup_code_hashes,
|
|
32854
33179
|
createdAt: row.created_at_ms,
|
|
32855
33180
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
33181
|
+
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
33182
|
+
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
32856
33183
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
32857
33184
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
32858
33185
|
smsPendingCodeHash: row.sms_pending_code_hash ?? undefined,
|
|
33186
|
+
smsPendingPurpose: row.sms_pending_purpose ?? undefined,
|
|
32859
33187
|
smsPhone: row.sms_phone ?? undefined,
|
|
33188
|
+
smsProviderReference: row.sms_provider_reference ?? undefined,
|
|
32860
33189
|
smsVerified: row.sms_verified,
|
|
32861
33190
|
totpFailedAttempts: row.totp_failed_attempts,
|
|
32862
33191
|
totpSecretCiphertext: row.totp_secret_ciphertext ?? undefined,
|
|
@@ -32865,40 +33194,118 @@ var toEnrollment = (row) => ({
|
|
|
32865
33194
|
userId: row.user_id
|
|
32866
33195
|
});
|
|
32867
33196
|
var createNeonMfaStore = (databaseUrl) => createPostgresMfaStore(createNeonDatabase(databaseUrl));
|
|
32868
|
-
var createPostgresMfaStore = (db) =>
|
|
32869
|
-
|
|
32870
|
-
|
|
32871
|
-
|
|
32872
|
-
|
|
32873
|
-
|
|
32874
|
-
|
|
32875
|
-
|
|
32876
|
-
|
|
32877
|
-
|
|
32878
|
-
|
|
32879
|
-
|
|
32880
|
-
|
|
32881
|
-
|
|
32882
|
-
|
|
32883
|
-
|
|
32884
|
-
|
|
32885
|
-
|
|
32886
|
-
|
|
32887
|
-
|
|
32888
|
-
|
|
32889
|
-
|
|
32890
|
-
|
|
32891
|
-
|
|
32892
|
-
|
|
32893
|
-
|
|
32894
|
-
|
|
32895
|
-
|
|
32896
|
-
|
|
32897
|
-
|
|
32898
|
-
|
|
32899
|
-
|
|
32900
|
-
|
|
32901
|
-
|
|
33197
|
+
var createPostgresMfaStore = (db) => {
|
|
33198
|
+
const toValues = (enrollment) => ({
|
|
33199
|
+
backup_code_hashes: enrollment.backupCodeHashes,
|
|
33200
|
+
created_at_ms: enrollment.createdAt,
|
|
33201
|
+
last_used_at_ms: enrollment.lastUsedAt ?? null,
|
|
33202
|
+
sms_challenge_id: enrollment.smsChallengeId ?? null,
|
|
33203
|
+
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
33204
|
+
sms_failed_attempts: enrollment.smsFailedAttempts ?? 0,
|
|
33205
|
+
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
33206
|
+
sms_pending_code_hash: enrollment.smsPendingCodeHash ?? null,
|
|
33207
|
+
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
33208
|
+
sms_phone: enrollment.smsPhone ?? null,
|
|
33209
|
+
sms_provider_reference: enrollment.smsProviderReference ?? null,
|
|
33210
|
+
sms_verified: enrollment.smsVerified,
|
|
33211
|
+
totp_failed_attempts: enrollment.totpFailedAttempts ?? 0,
|
|
33212
|
+
totp_secret_ciphertext: enrollment.totpSecretCiphertext ?? null,
|
|
33213
|
+
totp_verified: enrollment.totpVerified,
|
|
33214
|
+
updated_at_ms: enrollment.updatedAt,
|
|
33215
|
+
user_id: enrollment.userId
|
|
33216
|
+
});
|
|
33217
|
+
return {
|
|
33218
|
+
claimSmsChallenge: async ({
|
|
33219
|
+
challengeId,
|
|
33220
|
+
cooldownCutoff,
|
|
33221
|
+
enrollment
|
|
33222
|
+
}) => {
|
|
33223
|
+
const values = toValues({
|
|
33224
|
+
...enrollment,
|
|
33225
|
+
smsChallengeId: challengeId
|
|
33226
|
+
});
|
|
33227
|
+
const rows = await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
33228
|
+
set: {
|
|
33229
|
+
sms_challenge_id: challengeId,
|
|
33230
|
+
sms_code_sent_at_ms: enrollment.smsCodeSentAt ?? null,
|
|
33231
|
+
sms_failed_attempts: 0,
|
|
33232
|
+
sms_pending_code_expires_at_ms: enrollment.smsPendingCodeExpiresAt ?? null,
|
|
33233
|
+
sms_pending_code_hash: null,
|
|
33234
|
+
sms_pending_purpose: enrollment.smsPendingPurpose ?? null,
|
|
33235
|
+
sms_phone: enrollment.smsPhone ?? null,
|
|
33236
|
+
sms_provider_reference: null,
|
|
33237
|
+
sms_verified: enrollment.smsVerified,
|
|
33238
|
+
updated_at_ms: enrollment.updatedAt
|
|
33239
|
+
},
|
|
33240
|
+
setWhere: or(isNull(mfaEnrollmentsTable.sms_code_sent_at_ms), lte(mfaEnrollmentsTable.sms_code_sent_at_ms, cooldownCutoff)),
|
|
33241
|
+
target: mfaEnrollmentsTable.user_id
|
|
33242
|
+
}).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
33243
|
+
return rows.length === 1;
|
|
33244
|
+
},
|
|
33245
|
+
completeSmsChallenge: async ({
|
|
33246
|
+
challengeId,
|
|
33247
|
+
lastUsedAt,
|
|
33248
|
+
smsVerified,
|
|
33249
|
+
userId
|
|
33250
|
+
}) => {
|
|
33251
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
33252
|
+
last_used_at_ms: lastUsedAt,
|
|
33253
|
+
sms_challenge_id: null,
|
|
33254
|
+
sms_failed_attempts: 0,
|
|
33255
|
+
sms_pending_code_expires_at_ms: null,
|
|
33256
|
+
sms_pending_code_hash: null,
|
|
33257
|
+
sms_pending_purpose: null,
|
|
33258
|
+
sms_provider_reference: null,
|
|
33259
|
+
sms_verified: smsVerified,
|
|
33260
|
+
updated_at_ms: Date.now()
|
|
33261
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
33262
|
+
return rows.length === 1;
|
|
33263
|
+
},
|
|
33264
|
+
finalizeSmsChallenge: async (input) => {
|
|
33265
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
33266
|
+
sms_pending_code_expires_at_ms: input.expiresAt,
|
|
33267
|
+
sms_pending_code_hash: input.hash ?? null,
|
|
33268
|
+
sms_provider_reference: input.providerReference ?? null,
|
|
33269
|
+
updated_at_ms: Date.now()
|
|
33270
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, input.userId), eq(mfaEnrollmentsTable.sms_challenge_id, input.challengeId))).returning({ userId: mfaEnrollmentsTable.user_id });
|
|
33271
|
+
return rows.length === 1;
|
|
33272
|
+
},
|
|
33273
|
+
getEnrollment: async (userId) => {
|
|
33274
|
+
const [row] = await db.select().from(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId)).limit(1);
|
|
33275
|
+
return row ? toEnrollment(row) : undefined;
|
|
33276
|
+
},
|
|
33277
|
+
listEnrollments: async () => {
|
|
33278
|
+
const rows = await db.select().from(mfaEnrollmentsTable);
|
|
33279
|
+
return rows.map(toEnrollment);
|
|
33280
|
+
},
|
|
33281
|
+
recordSmsFailure: async ({ challengeId, maxAttempts, userId }) => {
|
|
33282
|
+
const rows = await db.update(mfaEnrollmentsTable).set({
|
|
33283
|
+
sms_failed_attempts: sql`${mfaEnrollmentsTable.sms_failed_attempts} + 1`,
|
|
33284
|
+
updated_at_ms: Date.now()
|
|
33285
|
+
}).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId), lte(mfaEnrollmentsTable.sms_failed_attempts, maxAttempts - 1))).returning({
|
|
33286
|
+
attempts: mfaEnrollmentsTable.sms_failed_attempts
|
|
33287
|
+
});
|
|
33288
|
+
return rows[0]?.attempts;
|
|
33289
|
+
},
|
|
33290
|
+
removeEnrollment: async (userId) => {
|
|
33291
|
+
await db.delete(mfaEnrollmentsTable).where(eq(mfaEnrollmentsTable.user_id, userId));
|
|
33292
|
+
},
|
|
33293
|
+
rollbackSmsChallenge: async ({ challengeId, previous, userId }) => {
|
|
33294
|
+
if (previous) {
|
|
33295
|
+
await db.update(mfaEnrollmentsTable).set(toValues(previous)).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
|
|
33296
|
+
return;
|
|
33297
|
+
}
|
|
33298
|
+
await db.delete(mfaEnrollmentsTable).where(and(eq(mfaEnrollmentsTable.user_id, userId), eq(mfaEnrollmentsTable.sms_challenge_id, challengeId)));
|
|
33299
|
+
},
|
|
33300
|
+
saveEnrollment: async (enrollment) => {
|
|
33301
|
+
const values = toValues(enrollment);
|
|
33302
|
+
await db.insert(mfaEnrollmentsTable).values(values).onConflictDoUpdate({
|
|
33303
|
+
set: values,
|
|
33304
|
+
target: mfaEnrollmentsTable.user_id
|
|
33305
|
+
});
|
|
33306
|
+
}
|
|
33307
|
+
};
|
|
33308
|
+
};
|
|
32902
33309
|
// src/audit/types.ts
|
|
32903
33310
|
var AUDIT_EVENT_TYPES = [
|
|
32904
33311
|
"account_deleted",
|
|
@@ -37234,6 +37641,19 @@ var mfaTotpLockoutMigration = {
|
|
|
37234
37641
|
id: "0003_totp_lockout",
|
|
37235
37642
|
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "totp_failed_attempts" smallint NOT NULL DEFAULT 0;'
|
|
37236
37643
|
};
|
|
37644
|
+
var mfaSmsDeliveryPolicyMigration = {
|
|
37645
|
+
id: "0004_sms_delivery_policy",
|
|
37646
|
+
sql: [
|
|
37647
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_code_sent_at_ms" bigint;',
|
|
37648
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_pending_purpose" text;',
|
|
37649
|
+
'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_provider_reference" text;'
|
|
37650
|
+
].join(`
|
|
37651
|
+
`)
|
|
37652
|
+
};
|
|
37653
|
+
var mfaSmsAtomicChallengeMigration = {
|
|
37654
|
+
id: "0005_sms_atomic_challenge",
|
|
37655
|
+
sql: 'ALTER TABLE "auth_mfa_enrollments" ADD COLUMN IF NOT EXISTS "sms_challenge_id" text;'
|
|
37656
|
+
};
|
|
37237
37657
|
var oidcResourceAudienceMigration = {
|
|
37238
37658
|
id: "0002_resource_audience",
|
|
37239
37659
|
sql: [
|
|
@@ -37288,7 +37708,9 @@ var blockMigrations = {
|
|
|
37288
37708
|
migrations: [
|
|
37289
37709
|
...initMigration("mfa", [mfaEnrollmentsTable]).migrations,
|
|
37290
37710
|
mfaSmsColumnsMigration,
|
|
37291
|
-
mfaTotpLockoutMigration
|
|
37711
|
+
mfaTotpLockoutMigration,
|
|
37712
|
+
mfaSmsDeliveryPolicyMigration,
|
|
37713
|
+
mfaSmsAtomicChallengeMigration
|
|
37292
37714
|
]
|
|
37293
37715
|
},
|
|
37294
37716
|
oidc: {
|
|
@@ -37801,6 +38223,7 @@ var buildAuthApplications = async (configuration) => {
|
|
|
37801
38223
|
credentials,
|
|
37802
38224
|
customProviders,
|
|
37803
38225
|
mfa,
|
|
38226
|
+
verificationProvider,
|
|
37804
38227
|
passwordless,
|
|
37805
38228
|
lockout,
|
|
37806
38229
|
sessions,
|
|
@@ -38033,7 +38456,8 @@ var buildAuthApplications = async (configuration) => {
|
|
|
38033
38456
|
auditedMfa ? mfaRoutes({
|
|
38034
38457
|
...auditedMfa,
|
|
38035
38458
|
authSessionStore,
|
|
38036
|
-
cookieSecure: resolvedCookieSecure
|
|
38459
|
+
cookieSecure: resolvedCookieSecure,
|
|
38460
|
+
verificationProvider
|
|
38037
38461
|
}) : new Elysia45,
|
|
38038
38462
|
passwordless ? passwordlessRoutes({
|
|
38039
38463
|
...passwordless,
|
|
@@ -38280,8 +38704,9 @@ export {
|
|
|
38280
38704
|
createPostgresScimTokenStore,
|
|
38281
38705
|
createNodeSamlAdapter,
|
|
38282
38706
|
createAuthContext,
|
|
38283
|
-
auth2 as auth
|
|
38707
|
+
auth2 as auth,
|
|
38708
|
+
VerificationProviderError
|
|
38284
38709
|
};
|
|
38285
38710
|
|
|
38286
|
-
//# debugId=
|
|
38711
|
+
//# debugId=82A372899BEA4F4564756E2164756E21
|
|
38287
38712
|
//# sourceMappingURL=server.js.map
|