@cairs-npm/express-auth-2fa 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +52 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -2
- package/dist/index.d.ts +41 -2
- package/dist/index.js +53 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -174,6 +174,17 @@ var configSchema = import_zod.z.object({
|
|
|
174
174
|
rpName: import_zod.z.string().min(1),
|
|
175
175
|
origins: import_zod.z.array(import_zod.z.string().url()).min(1)
|
|
176
176
|
}).optional(),
|
|
177
|
+
/**
|
|
178
|
+
* Allow POST /auth/login/start: sign in with the identifier and a verification method only
|
|
179
|
+
* (no password). Single-factor — enable only for apps that already sign in by code. Pass a
|
|
180
|
+
* function to allow it per request, e.g. only for `type: "badge"`.
|
|
181
|
+
*/
|
|
182
|
+
passwordlessLogin: import_zod.z.union([
|
|
183
|
+
import_zod.z.boolean(),
|
|
184
|
+
import_zod.z.custom(
|
|
185
|
+
(v) => typeof v === "function"
|
|
186
|
+
)
|
|
187
|
+
]).default(false),
|
|
177
188
|
/** 2-Step Verification state for users who have not chosen one yet. */
|
|
178
189
|
twoStepDefault: import_zod.z.boolean().default(false),
|
|
179
190
|
limits: limitSchema.default({}),
|
|
@@ -885,13 +896,42 @@ var AuthService = class {
|
|
|
885
896
|
await this.audit.record(user.id, "login_success", ctx);
|
|
886
897
|
return this.authenticate(user.id, req, res, ["pwd"], { rememberMe: input.rememberMe });
|
|
887
898
|
}
|
|
888
|
-
await
|
|
889
|
-
|
|
890
|
-
const active = await this.cols.methods.find({ userId: user.id, status: "active" }).toArray();
|
|
891
|
-
if (active.length === 0) {
|
|
899
|
+
const challenge = await this.issueLoginChallenge(user, ctx, now, false);
|
|
900
|
+
if (!challenge) {
|
|
892
901
|
await this.audit.record(user.id, "login_success", ctx);
|
|
893
902
|
return this.authenticate(user.id, req, res, ["pwd"], { rememberMe: input.rememberMe });
|
|
894
903
|
}
|
|
904
|
+
return challenge;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Passwordless sign-in (config.passwordlessLogin): the identifier alone starts the same
|
|
908
|
+
* verification step as a password sign-in. Always needs a method — there's no password to
|
|
909
|
+
* fall back on.
|
|
910
|
+
*/
|
|
911
|
+
async loginStart(req, input) {
|
|
912
|
+
const allow = this.config.passwordlessLogin;
|
|
913
|
+
const allowed = typeof allow === "function" ? await allow({ username: input.username, type: input.type }) : allow;
|
|
914
|
+
if (!allowed) throw apiError(404, "VALIDATION_ERROR", "Not found.");
|
|
915
|
+
const now = this.config.clock();
|
|
916
|
+
const ctx = ctxOf(req);
|
|
917
|
+
const user = await this.config.userAdapter.findByLogin(input.username, { type: input.type, passwordless: true });
|
|
918
|
+
if (!user) throw apiError(404, "INVALID_CREDENTIALS", "We couldn't find an account with that email.");
|
|
919
|
+
const cred = await this.getCredentials(user.id);
|
|
920
|
+
if (cred?.lockedUntil && cred.lockedUntil.getTime() > now.getTime()) {
|
|
921
|
+
throw apiError(423, "LOCKED", "Too many attempts. Try again later.", { retryAt: cred.lockedUntil });
|
|
922
|
+
}
|
|
923
|
+
const challenge = await this.issueLoginChallenge(user, ctx, now, true, input.type);
|
|
924
|
+
if (!challenge) {
|
|
925
|
+
throw apiError(409, "NO_METHODS", "We can't send you a code: this account has no email or verification method.");
|
|
926
|
+
}
|
|
927
|
+
return challenge;
|
|
928
|
+
}
|
|
929
|
+
/** Creates a login challenge over the user's active methods; null when there are none. */
|
|
930
|
+
async issueLoginChallenge(user, ctx, now, passwordless, clientType) {
|
|
931
|
+
await ensureEmailMethod(this.config, this.cols, user.id, user.email);
|
|
932
|
+
await pruneUnreadableMethods(this.config, this.cols, user.id, this.audit);
|
|
933
|
+
const active = await this.cols.methods.find({ userId: user.id, status: "active" }).toArray();
|
|
934
|
+
if (active.length === 0) return null;
|
|
895
935
|
const challengeId = randomId("chg");
|
|
896
936
|
await this.cols.challenges.insertOne({
|
|
897
937
|
_id: challengeId,
|
|
@@ -902,6 +942,8 @@ var AuthService = class {
|
|
|
902
942
|
smsSentAt: null,
|
|
903
943
|
smsSendCount: 0,
|
|
904
944
|
attempts: 0,
|
|
945
|
+
...passwordless ? { passwordless: true } : {},
|
|
946
|
+
...clientType ? { clientType } : {},
|
|
905
947
|
expiresAt: new Date(now.getTime() + this.config.ttl.challengeSec * 1e3),
|
|
906
948
|
createdAt: now
|
|
907
949
|
});
|
|
@@ -1135,7 +1177,7 @@ var MfaService = class {
|
|
|
1135
1177
|
await this.audit.record(ch.userId, "login_success", ctxOf(req));
|
|
1136
1178
|
const user = await this.config.userAdapter.findById(ch.userId);
|
|
1137
1179
|
const ua = req.headers["user-agent"];
|
|
1138
|
-
const result = await this.auth.authenticate(ch.userId, req, res, ["pwd", "otp"], {
|
|
1180
|
+
const result = await this.auth.authenticate(ch.userId, req, res, ch.passwordless ? ["otp"] : ["pwd", "otp"], {
|
|
1139
1181
|
rememberDevice: input.rememberDevice,
|
|
1140
1182
|
deviceLabel: typeof ua === "string" ? ua.slice(0, 80) : void 0
|
|
1141
1183
|
});
|
|
@@ -1383,7 +1425,7 @@ var PasskeyService = class {
|
|
|
1383
1425
|
);
|
|
1384
1426
|
await this.audit.record(ch.userId, "login_success", ctxOf(req));
|
|
1385
1427
|
const ua = req.headers["user-agent"];
|
|
1386
|
-
const result = await this.auth.authenticate(ch.userId, req, res, ["pwd", "hwk"], {
|
|
1428
|
+
const result = await this.auth.authenticate(ch.userId, req, res, ch.passwordless ? ["hwk"] : ["pwd", "hwk"], {
|
|
1387
1429
|
rememberDevice: input.rememberDevice,
|
|
1388
1430
|
deviceLabel: typeof ua === "string" ? ua.slice(0, 80) : void 0
|
|
1389
1431
|
});
|
|
@@ -1573,6 +1615,10 @@ function buildAuthRouter(config, cols, services) {
|
|
|
1573
1615
|
const input = parseBody(import_auth_2fa_contract.loginRequestSchema, req.body);
|
|
1574
1616
|
return services.auth.login(req, res, input);
|
|
1575
1617
|
}));
|
|
1618
|
+
router.post("/auth/login/start", rateLimit(cols, config, "login_ip", loginIpPerMin, 6e4), wrap(async (req) => {
|
|
1619
|
+
const input = parseBody(import_auth_2fa_contract.loginStartRequestSchema, req.body);
|
|
1620
|
+
return services.auth.loginStart(req, input);
|
|
1621
|
+
}));
|
|
1576
1622
|
router.post("/auth/mfa/send", rateLimit(cols, config, "mfa_send_ip", verifyIpPerMin, 6e4), wrap(async (req) => {
|
|
1577
1623
|
const input = parseBody(import_auth_2fa_contract.mfaSendRequestSchema, req.body);
|
|
1578
1624
|
return services.mfa.sendCode(input.challengeId, input.methodId);
|