@absolutejs/auth 0.58.0 → 0.59.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/README.md +9 -4
- package/dist/cli/migrate.js +18 -12
- package/dist/cli/migrate.js.map +4 -4
- package/dist/index.d.ts +6 -6
- package/dist/index.js +353 -123
- package/dist/index.js.map +14 -13
- package/dist/mfa/challenge.d.ts +1 -1
- package/dist/mfa/config.d.ts +3 -0
- package/dist/mfa/management.d.ts +3 -3
- package/dist/mfa/postgresMfaStore.d.ts +15 -0
- package/dist/mfa/recentAuth.d.ts +2 -0
- package/dist/mfa/routes.d.ts +6 -6
- package/dist/mfa/sms.d.ts +9 -4
- package/dist/mfa/totp.d.ts +3 -3
- package/dist/mfa/types.d.ts +31 -0
- package/dist/server.js +352 -123
- package/dist/server.js.map +14 -13
- package/dist/verification/types.d.ts +13 -2
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -6844,6 +6844,7 @@ 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;
|
|
@@ -6873,6 +6874,9 @@ class VerificationProviderError extends Error {
|
|
|
6873
6874
|
}
|
|
6874
6875
|
}
|
|
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
|
+
|
|
6876
6880
|
// src/mfa/sms.ts
|
|
6877
6881
|
var DECIMAL_RADIX2 = 10;
|
|
6878
6882
|
var MASK_VISIBLE_DIGITS = 4;
|
|
@@ -6888,6 +6892,46 @@ var issueSmsCode = async (codeLength, ttlMs) => {
|
|
|
6888
6892
|
const expiresAt = Date.now() + ttlMs;
|
|
6889
6893
|
return { code, expiresAt, hash };
|
|
6890
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
|
+
}
|
|
6891
6935
|
var checkWithVerificationProvider = async (provider, input) => {
|
|
6892
6936
|
try {
|
|
6893
6937
|
return { result: await provider.check(input) };
|
|
@@ -6904,7 +6948,9 @@ var issueAndStoreSmsCode = async ({
|
|
|
6904
6948
|
enrollment,
|
|
6905
6949
|
mfaStore,
|
|
6906
6950
|
onSendSmsCode,
|
|
6951
|
+
previousEnrollment,
|
|
6907
6952
|
purpose,
|
|
6953
|
+
resendCooldownMs,
|
|
6908
6954
|
verificationProvider,
|
|
6909
6955
|
ttlMs,
|
|
6910
6956
|
userId
|
|
@@ -6912,40 +6958,62 @@ var issueAndStoreSmsCode = async ({
|
|
|
6912
6958
|
const phone = enrollment.smsPhone;
|
|
6913
6959
|
if (phone === undefined)
|
|
6914
6960
|
return;
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
});
|
|
6922
|
-
await mfaStore.saveEnrollment({
|
|
6961
|
+
const challengeId = crypto.randomUUID();
|
|
6962
|
+
const now = Date.now();
|
|
6963
|
+
const claimed = await mfaStore.claimSmsChallenge({
|
|
6964
|
+
challengeId,
|
|
6965
|
+
cooldownCutoff: now - resendCooldownMs,
|
|
6966
|
+
enrollment: {
|
|
6923
6967
|
...enrollment,
|
|
6924
|
-
|
|
6968
|
+
smsChallengeId: challengeId,
|
|
6969
|
+
smsCodeSentAt: now,
|
|
6925
6970
|
smsFailedAttempts: 0,
|
|
6926
|
-
smsPendingCodeExpiresAt:
|
|
6971
|
+
smsPendingCodeExpiresAt: now + ttlMs,
|
|
6927
6972
|
smsPendingCodeHash: undefined,
|
|
6928
6973
|
smsPendingPurpose: purpose,
|
|
6929
|
-
smsProviderReference:
|
|
6930
|
-
updatedAt:
|
|
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
|
|
6931
6989
|
});
|
|
6932
6990
|
return started.expiresAt;
|
|
6933
6991
|
}
|
|
6934
6992
|
const { code, expiresAt, hash } = await issueSmsCode(codeLength, ttlMs);
|
|
6935
|
-
await mfaStore.
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
smsPendingCodeHash: hash,
|
|
6941
|
-
smsPendingPurpose: purpose,
|
|
6942
|
-
smsProviderReference: undefined,
|
|
6943
|
-
updatedAt: Date.now()
|
|
6993
|
+
const finalized = await mfaStore.finalizeSmsChallenge({
|
|
6994
|
+
challengeId,
|
|
6995
|
+
expiresAt,
|
|
6996
|
+
hash,
|
|
6997
|
+
userId
|
|
6944
6998
|
});
|
|
6945
|
-
|
|
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
|
+
}
|
|
6946
7011
|
return expiresAt;
|
|
6947
7012
|
};
|
|
6948
7013
|
var mapVerificationProviderError = (error) => {
|
|
7014
|
+
if (error instanceof SmsChallengeConflictError) {
|
|
7015
|
+
return { message: error.message, status: "Too Many Requests" };
|
|
7016
|
+
}
|
|
6949
7017
|
if (!(error instanceof VerificationProviderError))
|
|
6950
7018
|
return;
|
|
6951
7019
|
if (error.kind === "rate_limited") {
|
|
@@ -6974,6 +7042,7 @@ var mfaSmsRoutes = ({
|
|
|
6974
7042
|
authSessionStore,
|
|
6975
7043
|
getUserId,
|
|
6976
7044
|
mfaStore,
|
|
7045
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6977
7046
|
onMfaEnrolled,
|
|
6978
7047
|
onSendSmsCode,
|
|
6979
7048
|
verificationProvider,
|
|
@@ -7000,6 +7069,9 @@ var mfaSmsRoutes = ({
|
|
|
7000
7069
|
if (!userSession) {
|
|
7001
7070
|
return status("Unauthorized", "Authentication required");
|
|
7002
7071
|
}
|
|
7072
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7073
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7074
|
+
}
|
|
7003
7075
|
if (!isE164Phone(phone)) {
|
|
7004
7076
|
return status("Bad Request", "Phone must be in E.164 format");
|
|
7005
7077
|
}
|
|
@@ -7009,48 +7081,37 @@ var mfaSmsRoutes = ({
|
|
|
7009
7081
|
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
7010
7082
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
7011
7083
|
}
|
|
7012
|
-
let started;
|
|
7013
7084
|
try {
|
|
7014
|
-
|
|
7015
|
-
|
|
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,
|
|
7016
7103
|
purpose: "mfa_enrollment",
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7104
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
7105
|
+
ttlMs: smsCodeTtlMs,
|
|
7106
|
+
userId,
|
|
7107
|
+
verificationProvider
|
|
7108
|
+
});
|
|
7020
7109
|
} catch (error) {
|
|
7021
7110
|
const mapped = mapVerificationProviderError(error);
|
|
7022
7111
|
if (mapped === undefined)
|
|
7023
7112
|
throw error;
|
|
7024
7113
|
return status(mapped.status, mapped.message);
|
|
7025
7114
|
}
|
|
7026
|
-
const local = started ? undefined : await issueSmsCode(smsCodeLength, smsCodeTtlMs);
|
|
7027
|
-
await mfaStore.saveEnrollment({
|
|
7028
|
-
backupCodeHashes: existing?.backupCodeHashes ?? [],
|
|
7029
|
-
createdAt: existing?.createdAt ?? now,
|
|
7030
|
-
lastUsedAt: existing?.lastUsedAt,
|
|
7031
|
-
smsCodeSentAt: now,
|
|
7032
|
-
smsFailedAttempts: 0,
|
|
7033
|
-
smsPendingCodeExpiresAt: started?.expiresAt ?? local?.expiresAt,
|
|
7034
|
-
smsPendingCodeHash: local?.hash,
|
|
7035
|
-
smsPendingPurpose: "mfa_enrollment",
|
|
7036
|
-
smsPhone: phone,
|
|
7037
|
-
smsProviderReference: started?.reference,
|
|
7038
|
-
smsVerified: false,
|
|
7039
|
-
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
7040
|
-
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
7041
|
-
totpVerified: existing?.totpVerified ?? false,
|
|
7042
|
-
updatedAt: now,
|
|
7043
|
-
userId
|
|
7044
|
-
});
|
|
7045
|
-
if (local !== undefined && onSendSmsCode !== undefined) {
|
|
7046
|
-
await onSendSmsCode({
|
|
7047
|
-
code: local.code,
|
|
7048
|
-
expiresAt: local.expiresAt,
|
|
7049
|
-
phone,
|
|
7050
|
-
purpose: "mfa_enrollment",
|
|
7051
|
-
userId
|
|
7052
|
-
});
|
|
7053
|
-
}
|
|
7054
7115
|
return status("OK", { phone: maskPhone(phone) });
|
|
7055
7116
|
}, {
|
|
7056
7117
|
body: t11.Object({ phone: t11.String() }),
|
|
@@ -7069,15 +7130,19 @@ var mfaSmsRoutes = ({
|
|
|
7069
7130
|
if (!userSession) {
|
|
7070
7131
|
return status("Unauthorized", "Authentication required");
|
|
7071
7132
|
}
|
|
7133
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7134
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7135
|
+
}
|
|
7072
7136
|
const userId = getUserId(userSession.user);
|
|
7073
7137
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7074
7138
|
if (!enrollment?.smsPhone) {
|
|
7075
7139
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
7076
7140
|
}
|
|
7077
7141
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
7142
|
+
const challengeId = enrollment.smsChallengeId;
|
|
7078
7143
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
7079
7144
|
const providerReference = enrollment.smsProviderReference;
|
|
7080
|
-
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined) {
|
|
7145
|
+
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
7081
7146
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
7082
7147
|
}
|
|
7083
7148
|
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
@@ -7109,23 +7174,21 @@ var mfaSmsRoutes = ({
|
|
|
7109
7174
|
}
|
|
7110
7175
|
const codeValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
7111
7176
|
if (!codeValid) {
|
|
7112
|
-
await mfaStore.
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7177
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
7178
|
+
challengeId,
|
|
7179
|
+
maxAttempts: smsMaxAttempts,
|
|
7180
|
+
userId
|
|
7116
7181
|
});
|
|
7117
|
-
return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "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");
|
|
7118
7183
|
}
|
|
7119
|
-
await mfaStore.
|
|
7120
|
-
|
|
7121
|
-
smsFailedAttempts: 0,
|
|
7122
|
-
smsPendingCodeExpiresAt: undefined,
|
|
7123
|
-
smsPendingCodeHash: undefined,
|
|
7124
|
-
smsPendingPurpose: undefined,
|
|
7125
|
-
smsProviderReference: undefined,
|
|
7184
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
7185
|
+
challengeId,
|
|
7126
7186
|
smsVerified: true,
|
|
7127
|
-
|
|
7187
|
+
userId
|
|
7128
7188
|
});
|
|
7189
|
+
if (!completed) {
|
|
7190
|
+
return status("Bad Request", "SMS challenge is no longer active");
|
|
7191
|
+
}
|
|
7129
7192
|
await onMfaEnrolled?.({ userId });
|
|
7130
7193
|
return status("OK", { status: "enrolled" });
|
|
7131
7194
|
}, {
|
|
@@ -7202,7 +7265,9 @@ var mfaChallenge = ({
|
|
|
7202
7265
|
enrollment,
|
|
7203
7266
|
mfaStore,
|
|
7204
7267
|
onSendSmsCode,
|
|
7268
|
+
previousEnrollment: enrollment,
|
|
7205
7269
|
purpose: "mfa_challenge",
|
|
7270
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
7206
7271
|
ttlMs: smsCodeTtlMs,
|
|
7207
7272
|
userId: getUserId(user),
|
|
7208
7273
|
verificationProvider
|
|
@@ -7225,9 +7290,10 @@ var mfaChallenge = ({
|
|
|
7225
7290
|
return status("Bad Request", "SMS code required");
|
|
7226
7291
|
}
|
|
7227
7292
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
7293
|
+
const challengeId = enrollment.smsChallengeId;
|
|
7228
7294
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
7229
7295
|
const providerReference = enrollment.smsProviderReference;
|
|
7230
|
-
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined) {
|
|
7296
|
+
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
7231
7297
|
return status("Bad Request", "No SMS code in progress");
|
|
7232
7298
|
}
|
|
7233
7299
|
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
@@ -7263,27 +7329,26 @@ var mfaChallenge = ({
|
|
|
7263
7329
|
}
|
|
7264
7330
|
const smsValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
7265
7331
|
if (!smsValid) {
|
|
7266
|
-
await mfaStore.
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7332
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
7333
|
+
challengeId,
|
|
7334
|
+
maxAttempts: smsMaxAttempts,
|
|
7335
|
+
userId: getUserId(user)
|
|
7270
7336
|
});
|
|
7271
7337
|
await onMfaChallengeError?.({
|
|
7272
7338
|
error: new Error("invalid_mfa_code"),
|
|
7273
7339
|
userId: getUserId(user)
|
|
7274
7340
|
});
|
|
7275
|
-
return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "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");
|
|
7276
7342
|
}
|
|
7277
|
-
await mfaStore.
|
|
7278
|
-
|
|
7343
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
7344
|
+
challengeId,
|
|
7279
7345
|
lastUsedAt: Date.now(),
|
|
7280
|
-
|
|
7281
|
-
|
|
7282
|
-
smsPendingCodeHash: undefined,
|
|
7283
|
-
smsPendingPurpose: undefined,
|
|
7284
|
-
smsProviderReference: undefined,
|
|
7285
|
-
updatedAt: Date.now()
|
|
7346
|
+
smsVerified: true,
|
|
7347
|
+
userId: getUserId(user)
|
|
7286
7348
|
});
|
|
7349
|
+
if (!completed) {
|
|
7350
|
+
return status("Unauthorized", "SMS challenge is no longer active");
|
|
7351
|
+
}
|
|
7287
7352
|
return promote();
|
|
7288
7353
|
};
|
|
7289
7354
|
if (factor === "sms")
|
|
@@ -7345,6 +7410,7 @@ var mfaManagementRoutes = ({
|
|
|
7345
7410
|
authSessionStore,
|
|
7346
7411
|
getUserId,
|
|
7347
7412
|
managementRoute = "/auth/mfa",
|
|
7413
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
7348
7414
|
mfaStore
|
|
7349
7415
|
}) => new Elysia19().use(sessionStore()).get(managementRoute, async ({
|
|
7350
7416
|
cookie: { user_session_id },
|
|
@@ -7383,6 +7449,9 @@ var mfaManagementRoutes = ({
|
|
|
7383
7449
|
if (!userSession) {
|
|
7384
7450
|
return status("Unauthorized", "Authentication required");
|
|
7385
7451
|
}
|
|
7452
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7453
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7454
|
+
}
|
|
7386
7455
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
7387
7456
|
return status("OK", { status: "disabled" });
|
|
7388
7457
|
}, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) });
|
|
@@ -7396,6 +7465,7 @@ var mfaTotpRoutes = ({
|
|
|
7396
7465
|
encryptionKey,
|
|
7397
7466
|
getUserId,
|
|
7398
7467
|
issuer = DEFAULT_MFA_ISSUER,
|
|
7468
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
7399
7469
|
mfaStore,
|
|
7400
7470
|
onMfaEnrolled,
|
|
7401
7471
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
@@ -7413,6 +7483,9 @@ var mfaTotpRoutes = ({
|
|
|
7413
7483
|
if (!userSession) {
|
|
7414
7484
|
return status("Unauthorized", "Authentication required");
|
|
7415
7485
|
}
|
|
7486
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7487
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7488
|
+
}
|
|
7416
7489
|
const userId = getUserId(userSession.user);
|
|
7417
7490
|
const secret = generateTotpSecret();
|
|
7418
7491
|
const existing = await mfaStore.getEnrollment(userId);
|
|
@@ -7452,6 +7525,9 @@ var mfaTotpRoutes = ({
|
|
|
7452
7525
|
if (!userSession) {
|
|
7453
7526
|
return status("Unauthorized", "Authentication required");
|
|
7454
7527
|
}
|
|
7528
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
7529
|
+
return status("Unauthorized", "Recent authentication required");
|
|
7530
|
+
}
|
|
7455
7531
|
const userId = getUserId(userSession.user);
|
|
7456
7532
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
7457
7533
|
if (!enrollment?.totpSecretCiphertext) {
|
|
@@ -32990,14 +33066,85 @@ var cloneEnrollment = (value) => ({
|
|
|
32990
33066
|
var createInMemoryMfaStore = () => {
|
|
32991
33067
|
const enrollments = new Map;
|
|
32992
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
|
+
},
|
|
32993
33120
|
getEnrollment: async (userId) => {
|
|
32994
33121
|
const enrollment = enrollments.get(userId);
|
|
32995
33122
|
return enrollment ? cloneEnrollment(enrollment) : undefined;
|
|
32996
33123
|
},
|
|
32997
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
|
+
},
|
|
32998
33137
|
removeEnrollment: async (userId) => {
|
|
32999
33138
|
enrollments.delete(userId);
|
|
33000
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
|
+
},
|
|
33001
33148
|
saveEnrollment: async (enrollment) => {
|
|
33002
33149
|
enrollments.set(enrollment.userId, cloneEnrollment(enrollment));
|
|
33003
33150
|
}
|
|
@@ -33010,6 +33157,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
33010
33157
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
33011
33158
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
33012
33159
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
33160
|
+
sms_challenge_id: text("sms_challenge_id"),
|
|
33013
33161
|
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
33014
33162
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
33015
33163
|
sms_pending_code_expires_at_ms: bigint("sms_pending_code_expires_at_ms", {
|
|
@@ -33030,6 +33178,7 @@ var toEnrollment = (row) => ({
|
|
|
33030
33178
|
backupCodeHashes: row.backup_code_hashes,
|
|
33031
33179
|
createdAt: row.created_at_ms,
|
|
33032
33180
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
33181
|
+
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
33033
33182
|
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
33034
33183
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
33035
33184
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
@@ -33045,43 +33194,118 @@ var toEnrollment = (row) => ({
|
|
|
33045
33194
|
userId: row.user_id
|
|
33046
33195
|
});
|
|
33047
33196
|
var createNeonMfaStore = (databaseUrl) => createPostgresMfaStore(createNeonDatabase(databaseUrl));
|
|
33048
|
-
var createPostgresMfaStore = (db) =>
|
|
33049
|
-
|
|
33050
|
-
|
|
33051
|
-
|
|
33052
|
-
|
|
33053
|
-
|
|
33054
|
-
|
|
33055
|
-
|
|
33056
|
-
|
|
33057
|
-
|
|
33058
|
-
|
|
33059
|
-
|
|
33060
|
-
|
|
33061
|
-
|
|
33062
|
-
|
|
33063
|
-
|
|
33064
|
-
|
|
33065
|
-
|
|
33066
|
-
|
|
33067
|
-
|
|
33068
|
-
|
|
33069
|
-
|
|
33070
|
-
|
|
33071
|
-
|
|
33072
|
-
|
|
33073
|
-
|
|
33074
|
-
|
|
33075
|
-
|
|
33076
|
-
|
|
33077
|
-
|
|
33078
|
-
|
|
33079
|
-
|
|
33080
|
-
|
|
33081
|
-
|
|
33082
|
-
|
|
33083
|
-
|
|
33084
|
-
|
|
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
|
+
};
|
|
33085
33309
|
// src/audit/types.ts
|
|
33086
33310
|
var AUDIT_EVENT_TYPES = [
|
|
33087
33311
|
"account_deleted",
|
|
@@ -37426,6 +37650,10 @@ var mfaSmsDeliveryPolicyMigration = {
|
|
|
37426
37650
|
].join(`
|
|
37427
37651
|
`)
|
|
37428
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
|
+
};
|
|
37429
37657
|
var oidcResourceAudienceMigration = {
|
|
37430
37658
|
id: "0002_resource_audience",
|
|
37431
37659
|
sql: [
|
|
@@ -37481,7 +37709,8 @@ var blockMigrations = {
|
|
|
37481
37709
|
...initMigration("mfa", [mfaEnrollmentsTable]).migrations,
|
|
37482
37710
|
mfaSmsColumnsMigration,
|
|
37483
37711
|
mfaTotpLockoutMigration,
|
|
37484
|
-
mfaSmsDeliveryPolicyMigration
|
|
37712
|
+
mfaSmsDeliveryPolicyMigration,
|
|
37713
|
+
mfaSmsAtomicChallengeMigration
|
|
37485
37714
|
]
|
|
37486
37715
|
},
|
|
37487
37716
|
oidc: {
|
|
@@ -38479,5 +38708,5 @@ export {
|
|
|
38479
38708
|
VerificationProviderError
|
|
38480
38709
|
};
|
|
38481
38710
|
|
|
38482
|
-
//# debugId=
|
|
38711
|
+
//# debugId=82A372899BEA4F4564756E2164756E21
|
|
38483
38712
|
//# sourceMappingURL=server.js.map
|