@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/index.js
CHANGED
|
@@ -5848,6 +5848,7 @@ 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;
|
|
@@ -5877,6 +5878,9 @@ class VerificationProviderError extends Error {
|
|
|
5877
5878
|
}
|
|
5878
5879
|
}
|
|
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
|
+
|
|
5880
5884
|
// src/mfa/sms.ts
|
|
5881
5885
|
var DECIMAL_RADIX2 = 10;
|
|
5882
5886
|
var MASK_VISIBLE_DIGITS = 4;
|
|
@@ -5892,6 +5896,46 @@ var issueSmsCode = async (codeLength, ttlMs) => {
|
|
|
5892
5896
|
const expiresAt = Date.now() + ttlMs;
|
|
5893
5897
|
return { code, expiresAt, hash };
|
|
5894
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
|
+
}
|
|
5895
5939
|
var checkWithVerificationProvider = async (provider, input) => {
|
|
5896
5940
|
try {
|
|
5897
5941
|
return { result: await provider.check(input) };
|
|
@@ -5908,7 +5952,9 @@ var issueAndStoreSmsCode = async ({
|
|
|
5908
5952
|
enrollment,
|
|
5909
5953
|
mfaStore,
|
|
5910
5954
|
onSendSmsCode,
|
|
5955
|
+
previousEnrollment,
|
|
5911
5956
|
purpose,
|
|
5957
|
+
resendCooldownMs,
|
|
5912
5958
|
verificationProvider,
|
|
5913
5959
|
ttlMs,
|
|
5914
5960
|
userId
|
|
@@ -5916,40 +5962,62 @@ var issueAndStoreSmsCode = async ({
|
|
|
5916
5962
|
const phone = enrollment.smsPhone;
|
|
5917
5963
|
if (phone === undefined)
|
|
5918
5964
|
return;
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
});
|
|
5926
|
-
await mfaStore.saveEnrollment({
|
|
5965
|
+
const challengeId = crypto.randomUUID();
|
|
5966
|
+
const now = Date.now();
|
|
5967
|
+
const claimed = await mfaStore.claimSmsChallenge({
|
|
5968
|
+
challengeId,
|
|
5969
|
+
cooldownCutoff: now - resendCooldownMs,
|
|
5970
|
+
enrollment: {
|
|
5927
5971
|
...enrollment,
|
|
5928
|
-
|
|
5972
|
+
smsChallengeId: challengeId,
|
|
5973
|
+
smsCodeSentAt: now,
|
|
5929
5974
|
smsFailedAttempts: 0,
|
|
5930
|
-
smsPendingCodeExpiresAt:
|
|
5975
|
+
smsPendingCodeExpiresAt: now + ttlMs,
|
|
5931
5976
|
smsPendingCodeHash: undefined,
|
|
5932
5977
|
smsPendingPurpose: purpose,
|
|
5933
|
-
smsProviderReference:
|
|
5934
|
-
updatedAt:
|
|
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
|
|
5935
5993
|
});
|
|
5936
5994
|
return started.expiresAt;
|
|
5937
5995
|
}
|
|
5938
5996
|
const { code, expiresAt, hash } = await issueSmsCode(codeLength, ttlMs);
|
|
5939
|
-
await mfaStore.
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
smsPendingCodeHash: hash,
|
|
5945
|
-
smsPendingPurpose: purpose,
|
|
5946
|
-
smsProviderReference: undefined,
|
|
5947
|
-
updatedAt: Date.now()
|
|
5997
|
+
const finalized = await mfaStore.finalizeSmsChallenge({
|
|
5998
|
+
challengeId,
|
|
5999
|
+
expiresAt,
|
|
6000
|
+
hash,
|
|
6001
|
+
userId
|
|
5948
6002
|
});
|
|
5949
|
-
|
|
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
|
+
}
|
|
5950
6015
|
return expiresAt;
|
|
5951
6016
|
};
|
|
5952
6017
|
var mapVerificationProviderError = (error) => {
|
|
6018
|
+
if (error instanceof SmsChallengeConflictError) {
|
|
6019
|
+
return { message: error.message, status: "Too Many Requests" };
|
|
6020
|
+
}
|
|
5953
6021
|
if (!(error instanceof VerificationProviderError))
|
|
5954
6022
|
return;
|
|
5955
6023
|
if (error.kind === "rate_limited") {
|
|
@@ -5978,6 +6046,7 @@ var mfaSmsRoutes = ({
|
|
|
5978
6046
|
authSessionStore,
|
|
5979
6047
|
getUserId,
|
|
5980
6048
|
mfaStore,
|
|
6049
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
5981
6050
|
onMfaEnrolled,
|
|
5982
6051
|
onSendSmsCode,
|
|
5983
6052
|
verificationProvider,
|
|
@@ -6004,6 +6073,9 @@ var mfaSmsRoutes = ({
|
|
|
6004
6073
|
if (!userSession) {
|
|
6005
6074
|
return status("Unauthorized", "Authentication required");
|
|
6006
6075
|
}
|
|
6076
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6077
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6078
|
+
}
|
|
6007
6079
|
if (!isE164Phone(phone)) {
|
|
6008
6080
|
return status("Bad Request", "Phone must be in E.164 format");
|
|
6009
6081
|
}
|
|
@@ -6013,48 +6085,37 @@ var mfaSmsRoutes = ({
|
|
|
6013
6085
|
if (existing?.smsCodeSentAt !== undefined && now - existing.smsCodeSentAt < smsResendCooldownMs) {
|
|
6014
6086
|
return status("Too Many Requests", "SMS resend cooldown active");
|
|
6015
6087
|
}
|
|
6016
|
-
let started;
|
|
6017
6088
|
try {
|
|
6018
|
-
|
|
6019
|
-
|
|
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,
|
|
6020
6107
|
purpose: "mfa_enrollment",
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6108
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
6109
|
+
ttlMs: smsCodeTtlMs,
|
|
6110
|
+
userId,
|
|
6111
|
+
verificationProvider
|
|
6112
|
+
});
|
|
6024
6113
|
} catch (error) {
|
|
6025
6114
|
const mapped = mapVerificationProviderError(error);
|
|
6026
6115
|
if (mapped === undefined)
|
|
6027
6116
|
throw error;
|
|
6028
6117
|
return status(mapped.status, mapped.message);
|
|
6029
6118
|
}
|
|
6030
|
-
const local = started ? undefined : await issueSmsCode(smsCodeLength, smsCodeTtlMs);
|
|
6031
|
-
await mfaStore.saveEnrollment({
|
|
6032
|
-
backupCodeHashes: existing?.backupCodeHashes ?? [],
|
|
6033
|
-
createdAt: existing?.createdAt ?? now,
|
|
6034
|
-
lastUsedAt: existing?.lastUsedAt,
|
|
6035
|
-
smsCodeSentAt: now,
|
|
6036
|
-
smsFailedAttempts: 0,
|
|
6037
|
-
smsPendingCodeExpiresAt: started?.expiresAt ?? local?.expiresAt,
|
|
6038
|
-
smsPendingCodeHash: local?.hash,
|
|
6039
|
-
smsPendingPurpose: "mfa_enrollment",
|
|
6040
|
-
smsPhone: phone,
|
|
6041
|
-
smsProviderReference: started?.reference,
|
|
6042
|
-
smsVerified: false,
|
|
6043
|
-
totpFailedAttempts: existing?.totpFailedAttempts ?? 0,
|
|
6044
|
-
totpSecretCiphertext: existing?.totpSecretCiphertext,
|
|
6045
|
-
totpVerified: existing?.totpVerified ?? false,
|
|
6046
|
-
updatedAt: now,
|
|
6047
|
-
userId
|
|
6048
|
-
});
|
|
6049
|
-
if (local !== undefined && onSendSmsCode !== undefined) {
|
|
6050
|
-
await onSendSmsCode({
|
|
6051
|
-
code: local.code,
|
|
6052
|
-
expiresAt: local.expiresAt,
|
|
6053
|
-
phone,
|
|
6054
|
-
purpose: "mfa_enrollment",
|
|
6055
|
-
userId
|
|
6056
|
-
});
|
|
6057
|
-
}
|
|
6058
6119
|
return status("OK", { phone: maskPhone(phone) });
|
|
6059
6120
|
}, {
|
|
6060
6121
|
body: t11.Object({ phone: t11.String() }),
|
|
@@ -6073,15 +6134,19 @@ var mfaSmsRoutes = ({
|
|
|
6073
6134
|
if (!userSession) {
|
|
6074
6135
|
return status("Unauthorized", "Authentication required");
|
|
6075
6136
|
}
|
|
6137
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6138
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6139
|
+
}
|
|
6076
6140
|
const userId = getUserId(userSession.user);
|
|
6077
6141
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
6078
6142
|
if (!enrollment?.smsPhone) {
|
|
6079
6143
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
6080
6144
|
}
|
|
6081
6145
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6146
|
+
const challengeId = enrollment.smsChallengeId;
|
|
6082
6147
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
6083
6148
|
const providerReference = enrollment.smsProviderReference;
|
|
6084
|
-
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined) {
|
|
6149
|
+
if (enrollment.smsPendingPurpose !== "mfa_enrollment" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
6085
6150
|
return status("Bad Request", "No SMS enrollment in progress");
|
|
6086
6151
|
}
|
|
6087
6152
|
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
@@ -6113,23 +6178,21 @@ var mfaSmsRoutes = ({
|
|
|
6113
6178
|
}
|
|
6114
6179
|
const codeValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
6115
6180
|
if (!codeValid) {
|
|
6116
|
-
await mfaStore.
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6181
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
6182
|
+
challengeId,
|
|
6183
|
+
maxAttempts: smsMaxAttempts,
|
|
6184
|
+
userId
|
|
6120
6185
|
});
|
|
6121
|
-
return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Bad Request", providerResult?.status === "expired" ? "SMS code expired" : "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");
|
|
6122
6187
|
}
|
|
6123
|
-
await mfaStore.
|
|
6124
|
-
|
|
6125
|
-
smsFailedAttempts: 0,
|
|
6126
|
-
smsPendingCodeExpiresAt: undefined,
|
|
6127
|
-
smsPendingCodeHash: undefined,
|
|
6128
|
-
smsPendingPurpose: undefined,
|
|
6129
|
-
smsProviderReference: undefined,
|
|
6188
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
6189
|
+
challengeId,
|
|
6130
6190
|
smsVerified: true,
|
|
6131
|
-
|
|
6191
|
+
userId
|
|
6132
6192
|
});
|
|
6193
|
+
if (!completed) {
|
|
6194
|
+
return status("Bad Request", "SMS challenge is no longer active");
|
|
6195
|
+
}
|
|
6133
6196
|
await onMfaEnrolled?.({ userId });
|
|
6134
6197
|
return status("OK", { status: "enrolled" });
|
|
6135
6198
|
}, {
|
|
@@ -6206,7 +6269,9 @@ var mfaChallenge = ({
|
|
|
6206
6269
|
enrollment,
|
|
6207
6270
|
mfaStore,
|
|
6208
6271
|
onSendSmsCode,
|
|
6272
|
+
previousEnrollment: enrollment,
|
|
6209
6273
|
purpose: "mfa_challenge",
|
|
6274
|
+
resendCooldownMs: smsResendCooldownMs,
|
|
6210
6275
|
ttlMs: smsCodeTtlMs,
|
|
6211
6276
|
userId: getUserId(user),
|
|
6212
6277
|
verificationProvider
|
|
@@ -6229,9 +6294,10 @@ var mfaChallenge = ({
|
|
|
6229
6294
|
return status("Bad Request", "SMS code required");
|
|
6230
6295
|
}
|
|
6231
6296
|
const localCodeHash = enrollment.smsPendingCodeHash;
|
|
6297
|
+
const challengeId = enrollment.smsChallengeId;
|
|
6232
6298
|
const localCodeExpiresAt = enrollment.smsPendingCodeExpiresAt;
|
|
6233
6299
|
const providerReference = enrollment.smsProviderReference;
|
|
6234
|
-
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined) {
|
|
6300
|
+
if (enrollment.smsPendingPurpose !== "mfa_challenge" || localCodeExpiresAt === undefined || challengeId === undefined) {
|
|
6235
6301
|
return status("Bad Request", "No SMS code in progress");
|
|
6236
6302
|
}
|
|
6237
6303
|
if (verificationProvider === undefined && localCodeHash === undefined) {
|
|
@@ -6267,27 +6333,26 @@ var mfaChallenge = ({
|
|
|
6267
6333
|
}
|
|
6268
6334
|
const smsValid = providerResult ? providerResult.status === "approved" : localCodeHash !== undefined && await constantTimeEqual(await hashToken(code), localCodeHash);
|
|
6269
6335
|
if (!smsValid) {
|
|
6270
|
-
await mfaStore.
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6336
|
+
const attempts = await mfaStore.recordSmsFailure({
|
|
6337
|
+
challengeId,
|
|
6338
|
+
maxAttempts: smsMaxAttempts,
|
|
6339
|
+
userId: getUserId(user)
|
|
6274
6340
|
});
|
|
6275
6341
|
await onMfaChallengeError?.({
|
|
6276
6342
|
error: new Error("invalid_mfa_code"),
|
|
6277
6343
|
userId: getUserId(user)
|
|
6278
6344
|
});
|
|
6279
|
-
return status(providerResult?.status === "max_attempts_reached" ? "Too Many Requests" : "Unauthorized", providerResult?.status === "expired" ? "SMS code expired" : "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");
|
|
6280
6346
|
}
|
|
6281
|
-
await mfaStore.
|
|
6282
|
-
|
|
6347
|
+
const completed = await mfaStore.completeSmsChallenge({
|
|
6348
|
+
challengeId,
|
|
6283
6349
|
lastUsedAt: Date.now(),
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
smsPendingCodeHash: undefined,
|
|
6287
|
-
smsPendingPurpose: undefined,
|
|
6288
|
-
smsProviderReference: undefined,
|
|
6289
|
-
updatedAt: Date.now()
|
|
6350
|
+
smsVerified: true,
|
|
6351
|
+
userId: getUserId(user)
|
|
6290
6352
|
});
|
|
6353
|
+
if (!completed) {
|
|
6354
|
+
return status("Unauthorized", "SMS challenge is no longer active");
|
|
6355
|
+
}
|
|
6291
6356
|
return promote();
|
|
6292
6357
|
};
|
|
6293
6358
|
if (factor === "sms")
|
|
@@ -6349,6 +6414,7 @@ var mfaManagementRoutes = ({
|
|
|
6349
6414
|
authSessionStore,
|
|
6350
6415
|
getUserId,
|
|
6351
6416
|
managementRoute = "/auth/mfa",
|
|
6417
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6352
6418
|
mfaStore
|
|
6353
6419
|
}) => new Elysia19().use(sessionStore()).get(managementRoute, async ({
|
|
6354
6420
|
cookie: { user_session_id },
|
|
@@ -6387,6 +6453,9 @@ var mfaManagementRoutes = ({
|
|
|
6387
6453
|
if (!userSession) {
|
|
6388
6454
|
return status("Unauthorized", "Authentication required");
|
|
6389
6455
|
}
|
|
6456
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6457
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6458
|
+
}
|
|
6390
6459
|
await mfaStore.removeEnrollment(getUserId(userSession.user));
|
|
6391
6460
|
return status("OK", { status: "disabled" });
|
|
6392
6461
|
}, { cookie: t13.Cookie({ user_session_id: userSessionIdTypebox }) });
|
|
@@ -6400,6 +6469,7 @@ var mfaTotpRoutes = ({
|
|
|
6400
6469
|
encryptionKey,
|
|
6401
6470
|
getUserId,
|
|
6402
6471
|
issuer = DEFAULT_MFA_ISSUER,
|
|
6472
|
+
managementAuthMaxAgeMs = DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
6403
6473
|
mfaStore,
|
|
6404
6474
|
onMfaEnrolled,
|
|
6405
6475
|
totpSetupRoute = "/auth/mfa/totp/setup",
|
|
@@ -6417,6 +6487,9 @@ var mfaTotpRoutes = ({
|
|
|
6417
6487
|
if (!userSession) {
|
|
6418
6488
|
return status("Unauthorized", "Authentication required");
|
|
6419
6489
|
}
|
|
6490
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6491
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6492
|
+
}
|
|
6420
6493
|
const userId = getUserId(userSession.user);
|
|
6421
6494
|
const secret = generateTotpSecret();
|
|
6422
6495
|
const existing = await mfaStore.getEnrollment(userId);
|
|
@@ -6456,6 +6529,9 @@ var mfaTotpRoutes = ({
|
|
|
6456
6529
|
if (!userSession) {
|
|
6457
6530
|
return status("Unauthorized", "Authentication required");
|
|
6458
6531
|
}
|
|
6532
|
+
if (!hasRecentAuthentication(userSession, managementAuthMaxAgeMs)) {
|
|
6533
|
+
return status("Unauthorized", "Recent authentication required");
|
|
6534
|
+
}
|
|
6459
6535
|
const userId = getUserId(userSession.user);
|
|
6460
6536
|
const enrollment = await mfaStore.getEnrollment(userId);
|
|
6461
6537
|
if (!enrollment?.totpSecretCiphertext) {
|
|
@@ -31994,14 +32070,85 @@ var cloneEnrollment = (value) => ({
|
|
|
31994
32070
|
var createInMemoryMfaStore = () => {
|
|
31995
32071
|
const enrollments = new Map;
|
|
31996
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
|
+
},
|
|
31997
32124
|
getEnrollment: async (userId) => {
|
|
31998
32125
|
const enrollment = enrollments.get(userId);
|
|
31999
32126
|
return enrollment ? cloneEnrollment(enrollment) : undefined;
|
|
32000
32127
|
},
|
|
32001
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
|
+
},
|
|
32002
32141
|
removeEnrollment: async (userId) => {
|
|
32003
32142
|
enrollments.delete(userId);
|
|
32004
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
|
+
},
|
|
32005
32152
|
saveEnrollment: async (enrollment) => {
|
|
32006
32153
|
enrollments.set(enrollment.userId, cloneEnrollment(enrollment));
|
|
32007
32154
|
}
|
|
@@ -32014,6 +32161,7 @@ var mfaEnrollmentsTable = pgTable("auth_mfa_enrollments", {
|
|
|
32014
32161
|
backup_code_hashes: jsonb("backup_code_hashes").$type().notNull().default([]),
|
|
32015
32162
|
created_at_ms: bigint("created_at_ms", { mode: "number" }).notNull(),
|
|
32016
32163
|
last_used_at_ms: bigint("last_used_at_ms", { mode: "number" }),
|
|
32164
|
+
sms_challenge_id: text("sms_challenge_id"),
|
|
32017
32165
|
sms_code_sent_at_ms: bigint("sms_code_sent_at_ms", { mode: "number" }),
|
|
32018
32166
|
sms_failed_attempts: smallint("sms_failed_attempts").notNull().default(0),
|
|
32019
32167
|
sms_pending_code_expires_at_ms: bigint("sms_pending_code_expires_at_ms", {
|
|
@@ -32034,6 +32182,7 @@ var toEnrollment = (row) => ({
|
|
|
32034
32182
|
backupCodeHashes: row.backup_code_hashes,
|
|
32035
32183
|
createdAt: row.created_at_ms,
|
|
32036
32184
|
lastUsedAt: row.last_used_at_ms ?? undefined,
|
|
32185
|
+
smsChallengeId: row.sms_challenge_id ?? undefined,
|
|
32037
32186
|
smsCodeSentAt: row.sms_code_sent_at_ms ?? undefined,
|
|
32038
32187
|
smsFailedAttempts: row.sms_failed_attempts,
|
|
32039
32188
|
smsPendingCodeExpiresAt: row.sms_pending_code_expires_at_ms ?? undefined,
|
|
@@ -32049,43 +32198,118 @@ var toEnrollment = (row) => ({
|
|
|
32049
32198
|
userId: row.user_id
|
|
32050
32199
|
});
|
|
32051
32200
|
var createNeonMfaStore = (databaseUrl) => createPostgresMfaStore(createNeonDatabase(databaseUrl));
|
|
32052
|
-
var createPostgresMfaStore = (db) =>
|
|
32053
|
-
|
|
32054
|
-
|
|
32055
|
-
|
|
32056
|
-
|
|
32057
|
-
|
|
32058
|
-
|
|
32059
|
-
|
|
32060
|
-
|
|
32061
|
-
|
|
32062
|
-
|
|
32063
|
-
|
|
32064
|
-
|
|
32065
|
-
|
|
32066
|
-
|
|
32067
|
-
|
|
32068
|
-
|
|
32069
|
-
|
|
32070
|
-
|
|
32071
|
-
|
|
32072
|
-
|
|
32073
|
-
|
|
32074
|
-
|
|
32075
|
-
|
|
32076
|
-
|
|
32077
|
-
|
|
32078
|
-
|
|
32079
|
-
|
|
32080
|
-
|
|
32081
|
-
|
|
32082
|
-
|
|
32083
|
-
|
|
32084
|
-
|
|
32085
|
-
|
|
32086
|
-
|
|
32087
|
-
|
|
32088
|
-
|
|
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
|
+
};
|
|
32089
32313
|
// src/audit/types.ts
|
|
32090
32314
|
var AUDIT_EVENT_TYPES = [
|
|
32091
32315
|
"account_deleted",
|
|
@@ -36430,6 +36654,10 @@ var mfaSmsDeliveryPolicyMigration = {
|
|
|
36430
36654
|
].join(`
|
|
36431
36655
|
`)
|
|
36432
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
|
+
};
|
|
36433
36661
|
var oidcResourceAudienceMigration = {
|
|
36434
36662
|
id: "0002_resource_audience",
|
|
36435
36663
|
sql: [
|
|
@@ -36485,7 +36713,8 @@ var blockMigrations = {
|
|
|
36485
36713
|
...initMigration("mfa", [mfaEnrollmentsTable]).migrations,
|
|
36486
36714
|
mfaSmsColumnsMigration,
|
|
36487
36715
|
mfaTotpLockoutMigration,
|
|
36488
|
-
mfaSmsDeliveryPolicyMigration
|
|
36716
|
+
mfaSmsDeliveryPolicyMigration,
|
|
36717
|
+
mfaSmsAtomicChallengeMigration
|
|
36489
36718
|
]
|
|
36490
36719
|
},
|
|
36491
36720
|
oidc: {
|
|
@@ -37795,6 +38024,7 @@ export {
|
|
|
37795
38024
|
DEFAULT_ORGANIZATIONS_ROUTE,
|
|
37796
38025
|
DEFAULT_OIDC_ROUTE,
|
|
37797
38026
|
DEFAULT_MFA_SESSION_TTL_MS,
|
|
38027
|
+
DEFAULT_MFA_MANAGEMENT_AUTH_MAX_AGE_MS,
|
|
37798
38028
|
DEFAULT_MFA_ISSUER,
|
|
37799
38029
|
DEFAULT_MAGIC_LINK_TTL_MS,
|
|
37800
38030
|
DEFAULT_INVITATION_TTL_MS,
|
|
@@ -37810,5 +38040,5 @@ export {
|
|
|
37810
38040
|
AGENT_CLAIM_GRANT_TYPE
|
|
37811
38041
|
};
|
|
37812
38042
|
|
|
37813
|
-
//# debugId=
|
|
38043
|
+
//# debugId=FC3F27A521162E4064756E2164756E21
|
|
37814
38044
|
//# sourceMappingURL=index.js.map
|