@nxgt/janus 0.16.0 → 0.17.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 +99 -11
- package/dist/auth/config/index.d.ts +1 -1
- package/dist/auth/config/index.d.ts.map +1 -1
- package/dist/auth/config/janus-config.d.ts +24 -3
- package/dist/auth/config/janus-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-devices.d.ts +5 -0
- package/dist/auth/config/resolve-devices.d.ts.map +1 -0
- package/dist/auth/config/resolved-config.d.ts +4 -0
- package/dist/auth/config/resolved-config.d.ts.map +1 -1
- package/dist/auth/devices/hint.d.ts +30 -0
- package/dist/auth/devices/hint.d.ts.map +1 -0
- package/dist/auth/devices/index.d.ts +10 -0
- package/dist/auth/devices/index.d.ts.map +1 -0
- package/dist/auth/devices/report.d.ts +11 -0
- package/dist/auth/devices/report.d.ts.map +1 -0
- package/dist/auth/devices/token.d.ts +13 -0
- package/dist/auth/devices/token.d.ts.map +1 -0
- package/dist/auth/email-sign-in.d.ts +4 -2
- package/dist/auth/email-sign-in.d.ts.map +1 -1
- package/dist/auth/events.d.ts +18 -5
- package/dist/auth/events.d.ts.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/magic-link.d.ts.map +1 -1
- package/dist/auth/second-factor/challenge.d.ts +2 -2
- package/dist/auth/second-factor/challenge.d.ts.map +1 -1
- package/dist/auth/second-factor/flows.d.ts +6 -4
- package/dist/auth/second-factor/flows.d.ts.map +1 -1
- package/dist/auth/second-factor/recovery.d.ts +6 -2
- package/dist/auth/second-factor/recovery.d.ts.map +1 -1
- package/dist/auth/sessions/open-session.d.ts +7 -1
- package/dist/auth/sessions/open-session.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts.map +1 -1
- package/dist/auth/types/email-flows.d.ts +2 -2
- package/dist/auth/types/email-flows.d.ts.map +1 -1
- package/dist/auth/types/index.d.ts +1 -1
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/magic-link.d.ts +2 -2
- package/dist/auth/types/magic-link.d.ts.map +1 -1
- package/dist/auth/types/password.d.ts +10 -3
- package/dist/auth/types/password.d.ts.map +1 -1
- package/dist/auth/types/second-factor.d.ts +6 -3
- package/dist/auth/types/second-factor.d.ts.map +1 -1
- package/dist/auth/types/sign-in.d.ts +32 -0
- package/dist/auth/types/sign-in.d.ts.map +1 -1
- package/dist/auth/users/flow-types.d.ts +2 -1
- package/dist/auth/users/flow-types.d.ts.map +1 -1
- package/dist/auth/users/password-flows.d.ts.map +1 -1
- package/dist/auth/users/sign-in.d.ts +5 -1
- package/dist/auth/users/sign-in.d.ts.map +1 -1
- package/dist/index.js +172 -73
- package/dist/index.js.map +19 -15
- package/docs/README.md +2 -1
- package/docs/guide/devices.md +428 -0
- package/docs/guide/events.md +53 -8
- package/docs/guide/magic-link.md +25 -3
- package/docs/guide/second-factor.md +30 -5
- package/docs/guide/sessions.md +8 -0
- package/docs/guide/sign-in-code.md +22 -3
- package/docs/guide/step-up.md +33 -0
- package/docs/guide/users.md +3 -2
- package/docs/guide/vocabulary.md +4 -3
- package/docs/roadmap.md +12 -8
- package/docs/troubleshooting.md +210 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -281,6 +281,13 @@ function unseal(sealer, sealed, boundTo, where) {
|
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
// src/auth/config/resolve-devices.ts
|
|
285
|
+
function resolveDevices(config, where) {
|
|
286
|
+
if (config === undefined)
|
|
287
|
+
return null;
|
|
288
|
+
return { sealer: resolveSealer(config?.keys, `${where}: devices.keys`) };
|
|
289
|
+
}
|
|
290
|
+
|
|
284
291
|
// src/auth/config/resolve-second-factor.ts
|
|
285
292
|
function resolveSecondFactor(config, where) {
|
|
286
293
|
if (config === undefined)
|
|
@@ -365,7 +372,8 @@ function resolveConfig(config, where) {
|
|
|
365
372
|
tokenTtlMs: resolveTokenTtls(config, where),
|
|
366
373
|
cookie: resolveCookie(config.cookie ?? {}, where),
|
|
367
374
|
signInThrottle: resolveSignInThrottle(config.signIn, where),
|
|
368
|
-
secondFactor: resolveSecondFactor(config.secondFactor, where)
|
|
375
|
+
secondFactor: resolveSecondFactor(config.secondFactor, where),
|
|
376
|
+
devices: resolveDevices(config.devices, where)
|
|
369
377
|
};
|
|
370
378
|
}
|
|
371
379
|
function entriesOf(config, hasUsers) {
|
|
@@ -725,10 +733,105 @@ function assertStores(stores, where) {
|
|
|
725
733
|
return { collectExpired: collect !== undefined };
|
|
726
734
|
}
|
|
727
735
|
|
|
736
|
+
// src/auth/devices/token.ts
|
|
737
|
+
import { randomBytes as randomBytes3, timingSafeEqual as timingSafeEqual2 } from "node:crypto";
|
|
738
|
+
|
|
739
|
+
// src/auth/derived-keys.ts
|
|
740
|
+
import { createHmac, hkdfSync } from "node:crypto";
|
|
741
|
+
var derived = new WeakMap;
|
|
742
|
+
function derivedKey(sealer, purpose, keyId = sealer.sealWith) {
|
|
743
|
+
let keys = derived.get(sealer);
|
|
744
|
+
if (keys === undefined) {
|
|
745
|
+
keys = new Map;
|
|
746
|
+
derived.set(sealer, keys);
|
|
747
|
+
}
|
|
748
|
+
const name = `${purpose}\x00${keyId}`;
|
|
749
|
+
let key = keys.get(name);
|
|
750
|
+
if (key === undefined) {
|
|
751
|
+
const secret = sealer.keys.get(keyId);
|
|
752
|
+
if (secret === undefined)
|
|
753
|
+
return;
|
|
754
|
+
key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), purpose, 32));
|
|
755
|
+
keys.set(name, key);
|
|
756
|
+
}
|
|
757
|
+
return key;
|
|
758
|
+
}
|
|
759
|
+
function keyedHash(key, parts) {
|
|
760
|
+
return createHmac("sha256", key).update(parts.join("\x00")).digest();
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/auth/devices/token.ts
|
|
764
|
+
var PURPOSE = "janus/devices/v1";
|
|
765
|
+
var TOKEN = /^d1\.([A-Za-z0-9_-]{1,64})\.([A-Za-z0-9_-]{22})\.([A-Za-z0-9_-]{43})$/;
|
|
766
|
+
function macOf(key, userId, deviceId) {
|
|
767
|
+
return keyedHash(key, [userId, deviceId]);
|
|
768
|
+
}
|
|
769
|
+
function signed(sealer, userId, deviceId) {
|
|
770
|
+
const key = derivedKey(sealer, PURPOSE);
|
|
771
|
+
const mac = macOf(key, userId, deviceId).toString("base64url");
|
|
772
|
+
return `d1.${sealer.sealWith}.${deviceId}.${mac}`;
|
|
773
|
+
}
|
|
774
|
+
function mintDeviceToken(sealer, userId) {
|
|
775
|
+
return signed(sealer, userId, randomBytes3(16).toString("base64url"));
|
|
776
|
+
}
|
|
777
|
+
function knownDeviceToken(sealer, userId, presented) {
|
|
778
|
+
const [, keyId, deviceId, mac] = TOKEN.exec(presented) ?? [];
|
|
779
|
+
if (keyId === undefined || deviceId === undefined || mac === undefined) {
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
const key = derivedKey(sealer, PURPOSE, keyId);
|
|
783
|
+
if (key === undefined)
|
|
784
|
+
return null;
|
|
785
|
+
const expected = macOf(key, userId, deviceId);
|
|
786
|
+
const given = Buffer.from(mac, "base64url");
|
|
787
|
+
if (given.length !== expected.length || !timingSafeEqual2(given, expected)) {
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
return keyId === sealer.sealWith ? presented : signed(sealer, userId, deviceId);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// src/auth/devices/hint.ts
|
|
794
|
+
var UNTRACKED = Object.freeze({
|
|
795
|
+
newDevice: false,
|
|
796
|
+
deviceToken: null
|
|
797
|
+
});
|
|
798
|
+
function deviceHint(context, options, where) {
|
|
799
|
+
if (options !== undefined && (typeof options !== "object" || options === null)) {
|
|
800
|
+
throw new TypeError(`${where}: options must be { device }, or left out — the token goes under device`);
|
|
801
|
+
}
|
|
802
|
+
const device = options?.device;
|
|
803
|
+
if (device === undefined)
|
|
804
|
+
return;
|
|
805
|
+
if (device !== null && typeof device !== "string") {
|
|
806
|
+
throw new TypeError(`${where}: options.device must be the device token the client holds, or null when it holds none`);
|
|
807
|
+
}
|
|
808
|
+
if (context.config.devices === null) {
|
|
809
|
+
throw new TypeError(`${where}: a device was given, but janus() has no devices — pass devices: { keys }`);
|
|
810
|
+
}
|
|
811
|
+
return device;
|
|
812
|
+
}
|
|
813
|
+
function deviceOutcome(context, userId, hint, signUp) {
|
|
814
|
+
const sealer = context.config.devices?.sealer;
|
|
815
|
+
if (hint === undefined || sealer === undefined)
|
|
816
|
+
return UNTRACKED;
|
|
817
|
+
const known = hint === null || signUp ? null : knownDeviceToken(sealer, userId, hint);
|
|
818
|
+
if (known !== null)
|
|
819
|
+
return { newDevice: false, deviceToken: known };
|
|
820
|
+
return {
|
|
821
|
+
newDevice: !signUp,
|
|
822
|
+
deviceToken: mintDeviceToken(sealer, userId)
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
// src/auth/devices/report.ts
|
|
826
|
+
async function reportNewDevice(context, result) {
|
|
827
|
+
if (result.status !== "signedIn" || !result.newDevice)
|
|
828
|
+
return;
|
|
829
|
+
await emit(context, "user.newDeviceSignedIn", result.user, result.session.createdAt, { sessionId: result.session.id });
|
|
830
|
+
}
|
|
728
831
|
// src/auth/secrets.ts
|
|
729
|
-
import { createHash, randomBytes as
|
|
832
|
+
import { createHash, randomBytes as randomBytes4 } from "node:crypto";
|
|
730
833
|
function mintSecret() {
|
|
731
|
-
return
|
|
834
|
+
return randomBytes4(32).toString("base64url");
|
|
732
835
|
}
|
|
733
836
|
function hashSecret(secret) {
|
|
734
837
|
return createHash("sha256").update(secret).digest("hex");
|
|
@@ -739,7 +842,7 @@ function toSession(record) {
|
|
|
739
842
|
const { tokenHash: _, ...session } = record;
|
|
740
843
|
return session;
|
|
741
844
|
}
|
|
742
|
-
async function openSession(context, type, user) {
|
|
845
|
+
async function openSession(context, type, user, device, signUp = false) {
|
|
743
846
|
const now = context.clock.now();
|
|
744
847
|
const token = mintSecret();
|
|
745
848
|
const record = {
|
|
@@ -756,7 +859,8 @@ async function openSession(context, type, user) {
|
|
|
756
859
|
status: "signedIn",
|
|
757
860
|
user: toUser(user),
|
|
758
861
|
session: toSession(record),
|
|
759
|
-
token
|
|
862
|
+
token,
|
|
863
|
+
...deviceOutcome(context, user.id, device, signUp)
|
|
760
864
|
};
|
|
761
865
|
}
|
|
762
866
|
// src/auth/sessions/presented-token.ts
|
|
@@ -905,7 +1009,7 @@ function sharedApi(context) {
|
|
|
905
1009
|
};
|
|
906
1010
|
}
|
|
907
1011
|
// src/auth/one-time/codes.ts
|
|
908
|
-
import { timingSafeEqual as
|
|
1012
|
+
import { timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
909
1013
|
|
|
910
1014
|
// src/auth/one-time/refusals.ts
|
|
911
1015
|
function refuseStale(type, user, token, where, noun) {
|
|
@@ -974,7 +1078,7 @@ function codeMatches(token, challenge, code) {
|
|
|
974
1078
|
return false;
|
|
975
1079
|
const given = Buffer.from(hashCode(challenge, code));
|
|
976
1080
|
const held = Buffer.from(token.codeHash);
|
|
977
|
-
return given.length === held.length &&
|
|
1081
|
+
return given.length === held.length && timingSafeEqual3(given, held);
|
|
978
1082
|
}
|
|
979
1083
|
// src/auth/one-time/issue.ts
|
|
980
1084
|
import { randomInt } from "node:crypto";
|
|
@@ -1201,11 +1305,11 @@ function emailFlows(context, type, at) {
|
|
|
1201
1305
|
};
|
|
1202
1306
|
}
|
|
1203
1307
|
// src/auth/totp.ts
|
|
1204
|
-
import { createHmac, randomBytes as
|
|
1308
|
+
import { createHmac as createHmac2, randomBytes as randomBytes5 } from "node:crypto";
|
|
1205
1309
|
var TOTP = { digits: 6, stepMs: 30000, drift: 1 };
|
|
1206
1310
|
var BASE32 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
1207
1311
|
function mintTotpSecret() {
|
|
1208
|
-
return toBase32(
|
|
1312
|
+
return toBase32(randomBytes5(20));
|
|
1209
1313
|
}
|
|
1210
1314
|
function toBase32(bytes) {
|
|
1211
1315
|
let bits = 0;
|
|
@@ -1244,7 +1348,7 @@ var stepAt = (now) => Math.floor(now.getTime() / TOTP.stepMs);
|
|
|
1244
1348
|
function codeAt(secret, step) {
|
|
1245
1349
|
const counter = Buffer.alloc(8);
|
|
1246
1350
|
counter.writeBigUInt64BE(BigInt(step));
|
|
1247
|
-
const digest =
|
|
1351
|
+
const digest = createHmac2("sha1", secret).update(counter).digest();
|
|
1248
1352
|
const offset = digest[19] & 15;
|
|
1249
1353
|
const binary = digest.readUInt32BE(offset) & 2147483647;
|
|
1250
1354
|
return String(binary % 10 ** TOTP.digits).padStart(TOTP.digits, "0");
|
|
@@ -1343,7 +1447,7 @@ async function keepOnlyLatest(context, userId, kind, secret) {
|
|
|
1343
1447
|
await context.store.tokens.spendUserTokens(userId, kind, context.clock.now(), hashSecret(secret));
|
|
1344
1448
|
}
|
|
1345
1449
|
async function finishEmailSignIn(context, type, finish, signIn) {
|
|
1346
|
-
const { user, token, where, noun } = signIn;
|
|
1450
|
+
const { user, token, where, noun, device } = signIn;
|
|
1347
1451
|
refuseStale(type, user, token, where, noun);
|
|
1348
1452
|
if (!user.active) {
|
|
1349
1453
|
throw new UserInactiveError2(`${where}: the user is inactive`, {
|
|
@@ -1351,9 +1455,10 @@ async function finishEmailSignIn(context, type, finish, signIn) {
|
|
|
1351
1455
|
userType: type.name
|
|
1352
1456
|
});
|
|
1353
1457
|
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1458
|
+
const proved = user.emailVerifiedAt !== null ? user : await proveFirstEmail(context, type, user, where);
|
|
1459
|
+
const result = await finish(proved, where, device);
|
|
1460
|
+
await reportNewDevice(context, result);
|
|
1461
|
+
return result;
|
|
1357
1462
|
}
|
|
1358
1463
|
|
|
1359
1464
|
// src/auth/magic-link.ts
|
|
@@ -1362,8 +1467,10 @@ function magicLinkFlows(context, type, at, finish) {
|
|
|
1362
1467
|
async request(email) {
|
|
1363
1468
|
return requestLink(context, type, String(email));
|
|
1364
1469
|
},
|
|
1365
|
-
async confirm(token) {
|
|
1366
|
-
|
|
1470
|
+
async confirm(token, options) {
|
|
1471
|
+
const where = at("magicLink.confirm");
|
|
1472
|
+
const device = deviceHint(context, options, where);
|
|
1473
|
+
return confirmLink(context, type, finish, String(token), where, device);
|
|
1367
1474
|
}
|
|
1368
1475
|
};
|
|
1369
1476
|
}
|
|
@@ -1381,46 +1488,23 @@ async function requestLink(context, type, email) {
|
|
|
1381
1488
|
await keepOnlyLatest(context, record.id, "magicLink", secret);
|
|
1382
1489
|
return { token: secret, email: address, expiresAt, user: toUser(record) };
|
|
1383
1490
|
}
|
|
1384
|
-
async function confirmLink(context, type, finish, secret, where) {
|
|
1491
|
+
async function confirmLink(context, type, finish, secret, where, device) {
|
|
1385
1492
|
const { token, user } = await redeemEmailToken(context, type, "magicLink", secret, where);
|
|
1386
1493
|
return finishEmailSignIn(context, type, finish, {
|
|
1387
1494
|
user,
|
|
1388
1495
|
token,
|
|
1389
1496
|
where,
|
|
1390
|
-
noun: "token"
|
|
1497
|
+
noun: "token",
|
|
1498
|
+
device
|
|
1391
1499
|
});
|
|
1392
1500
|
}
|
|
1393
1501
|
|
|
1394
|
-
// src/auth/derived-keys.ts
|
|
1395
|
-
import { createHmac as createHmac2, hkdfSync } from "node:crypto";
|
|
1396
|
-
var derived = new WeakMap;
|
|
1397
|
-
function derivedKey(sealer, purpose, keyId = sealer.sealWith) {
|
|
1398
|
-
let keys = derived.get(sealer);
|
|
1399
|
-
if (keys === undefined) {
|
|
1400
|
-
keys = new Map;
|
|
1401
|
-
derived.set(sealer, keys);
|
|
1402
|
-
}
|
|
1403
|
-
const name = `${purpose}\x00${keyId}`;
|
|
1404
|
-
let key = keys.get(name);
|
|
1405
|
-
if (key === undefined) {
|
|
1406
|
-
const secret = sealer.keys.get(keyId);
|
|
1407
|
-
if (secret === undefined)
|
|
1408
|
-
return;
|
|
1409
|
-
key = Buffer.from(hkdfSync("sha256", secret, Buffer.alloc(0), purpose, 32));
|
|
1410
|
-
keys.set(name, key);
|
|
1411
|
-
}
|
|
1412
|
-
return key;
|
|
1413
|
-
}
|
|
1414
|
-
function keyedHash(key, parts) {
|
|
1415
|
-
return createHmac2("sha256", key).update(parts.join("\x00")).digest();
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
1502
|
// src/auth/sign-in-attempts.ts
|
|
1419
|
-
var
|
|
1420
|
-
var UNSEALED = Buffer.from(
|
|
1503
|
+
var PURPOSE2 = "janus/sign-in/attempts/v1";
|
|
1504
|
+
var UNSEALED = Buffer.from(PURPOSE2);
|
|
1421
1505
|
function counterKey(context) {
|
|
1422
1506
|
const sealer = context.config.secondFactor?.sealer;
|
|
1423
|
-
return sealer === undefined ? UNSEALED : derivedKey(sealer,
|
|
1507
|
+
return sealer === undefined ? UNSEALED : derivedKey(sealer, PURPOSE2);
|
|
1424
1508
|
}
|
|
1425
1509
|
var keyed = (key, parts) => keyedHash(key, parts).toString("base64url");
|
|
1426
1510
|
async function countLogin(context, type, login, where) {
|
|
@@ -1515,13 +1599,14 @@ function challengeFlows(context, type, at) {
|
|
|
1515
1599
|
userId: record.id
|
|
1516
1600
|
};
|
|
1517
1601
|
},
|
|
1518
|
-
async confirm(challenge, code) {
|
|
1519
|
-
return confirmChallenge(context, type, challenge, code, at);
|
|
1602
|
+
async confirm(challenge, code, options) {
|
|
1603
|
+
return confirmChallenge(context, type, challenge, code, at, options);
|
|
1520
1604
|
}
|
|
1521
1605
|
};
|
|
1522
1606
|
}
|
|
1523
|
-
async function confirmChallenge(context, type, challenge, code, at) {
|
|
1607
|
+
async function confirmChallenge(context, type, challenge, code, at, options) {
|
|
1524
1608
|
const where = at("secondFactor.confirm");
|
|
1609
|
+
const device = deviceHint(context, options, where);
|
|
1525
1610
|
const opened = await openChallenge(context, type, challenge, where);
|
|
1526
1611
|
const { record } = opened;
|
|
1527
1612
|
const now = context.clock.now();
|
|
@@ -1531,7 +1616,9 @@ async function confirmChallenge(context, type, challenge, code, at) {
|
|
|
1531
1616
|
const written = await context.store.users.updateUser(record.id, { secondFactor: accepted, updatedAt: now }, record.version);
|
|
1532
1617
|
await spendChallenge(context, opened.secret, where);
|
|
1533
1618
|
await restartSignInCount(context, type, written, where);
|
|
1534
|
-
|
|
1619
|
+
const signedIn = await openSession(context, type, written, device);
|
|
1620
|
+
await reportNewDevice(context, signedIn);
|
|
1621
|
+
return signedIn;
|
|
1535
1622
|
}
|
|
1536
1623
|
|
|
1537
1624
|
// src/auth/second-factor/codes-left.ts
|
|
@@ -1542,12 +1629,12 @@ async function recoveryCodesLeftOf(context, type, user, where) {
|
|
|
1542
1629
|
}
|
|
1543
1630
|
|
|
1544
1631
|
// src/auth/second-factor/recovery-codes.ts
|
|
1545
|
-
import { randomInt as randomInt2, timingSafeEqual as
|
|
1632
|
+
import { randomInt as randomInt2, timingSafeEqual as timingSafeEqual4 } from "node:crypto";
|
|
1546
1633
|
var RECOVERY_CODES = 10;
|
|
1547
1634
|
var ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
|
|
1548
1635
|
var LENGTH = 10;
|
|
1549
1636
|
var SHAPE = /^[0-9a-hjkmnp-tv-z]{10}$/;
|
|
1550
|
-
var
|
|
1637
|
+
var PURPOSE3 = "janus/second-factor/recovery-codes/v1";
|
|
1551
1638
|
function mintRecoveryCodes() {
|
|
1552
1639
|
return Array.from({ length: RECOVERY_CODES }, () => {
|
|
1553
1640
|
let code = "";
|
|
@@ -1561,7 +1648,7 @@ function readRecoveryCode(typed) {
|
|
|
1561
1648
|
const code = typed.toLowerCase().replace(/[\s-]/g, "").replace(/o/g, "0").replace(/[il]/g, "1");
|
|
1562
1649
|
return SHAPE.test(code) ? code : null;
|
|
1563
1650
|
}
|
|
1564
|
-
var macKey = (sealer, keyId) => derivedKey(sealer,
|
|
1651
|
+
var macKey = (sealer, keyId) => derivedKey(sealer, PURPOSE3, keyId);
|
|
1565
1652
|
var mac = (key, userId, code) => keyedHash(key, [userId, code]);
|
|
1566
1653
|
function hashRecoveryCode(sealer, userId, code) {
|
|
1567
1654
|
const read = readRecoveryCode(code) ?? code;
|
|
@@ -1584,7 +1671,7 @@ function findRecoveryCode(sealer, userId, hashes, typed, where) {
|
|
|
1584
1671
|
return;
|
|
1585
1672
|
const held = Buffer.from(body, "base64url");
|
|
1586
1673
|
const given = mac(key, userId, code);
|
|
1587
|
-
if (held.length === given.length &&
|
|
1674
|
+
if (held.length === given.length && timingSafeEqual4(held, given)) {
|
|
1588
1675
|
found = found === -1 ? index : found;
|
|
1589
1676
|
}
|
|
1590
1677
|
});
|
|
@@ -1672,7 +1759,7 @@ function accountOf(type, record, where) {
|
|
|
1672
1759
|
// src/auth/second-factor/app-code-attempts.ts
|
|
1673
1760
|
var APP_CODE_WINDOW_MS = 15 * 60000;
|
|
1674
1761
|
var WINDOW_MINUTES = APP_CODE_WINDOW_MS / 60000;
|
|
1675
|
-
var
|
|
1762
|
+
var PURPOSE4 = "janus/second-factor/regenerate-attempts/v1";
|
|
1676
1763
|
function linkHash(sealer, record, window, link) {
|
|
1677
1764
|
const parts = [
|
|
1678
1765
|
record.id,
|
|
@@ -1680,7 +1767,7 @@ function linkHash(sealer, record, window, link) {
|
|
|
1680
1767
|
String(record.secondFactor?.lastStep ?? ""),
|
|
1681
1768
|
String(link)
|
|
1682
1769
|
];
|
|
1683
|
-
const key = derivedKey(sealer,
|
|
1770
|
+
const key = derivedKey(sealer, PURPOSE4);
|
|
1684
1771
|
return hashSecret(keyedHash(key, parts).toString("base64url"));
|
|
1685
1772
|
}
|
|
1686
1773
|
async function countAppCodeAttempt(context, sealer, record, where) {
|
|
@@ -1750,9 +1837,11 @@ function lifecycleFlows(context, type, at) {
|
|
|
1750
1837
|
}
|
|
1751
1838
|
|
|
1752
1839
|
// src/auth/second-factor/recovery.ts
|
|
1753
|
-
async function recoverWithCode(context, type,
|
|
1840
|
+
async function recoverWithCode(context, type, given, at) {
|
|
1754
1841
|
const where = at("secondFactor.recover");
|
|
1755
|
-
const
|
|
1842
|
+
const device = deviceHint(context, given.options, where);
|
|
1843
|
+
const { code } = given;
|
|
1844
|
+
const opened = await openChallenge(context, type, given.challenge, where);
|
|
1756
1845
|
const { record } = opened;
|
|
1757
1846
|
const factor = record.secondFactor;
|
|
1758
1847
|
const found = findRecoveryCode(opened.configured.sealer, record.id, factor.recoveryCodes, String(code), where);
|
|
@@ -1763,14 +1852,16 @@ async function recoverWithCode(context, type, challenge, code, at) {
|
|
|
1763
1852
|
secondFactor: { ...factor, recoveryCodes },
|
|
1764
1853
|
updatedAt: context.clock.now()
|
|
1765
1854
|
}, record.version);
|
|
1855
|
+
let signedIn;
|
|
1766
1856
|
try {
|
|
1767
1857
|
await spendChallenge(context, opened.secret, where);
|
|
1768
1858
|
await restartSignInCount(context, type, written, where);
|
|
1769
|
-
|
|
1770
|
-
return { ...signedIn, recoveryCodesLeft: recoveryCodes.length };
|
|
1859
|
+
signedIn = await openSession(context, type, written, device);
|
|
1771
1860
|
} finally {
|
|
1772
1861
|
await emit(context, "user.recoveryCodeUsed", written, written.updatedAt);
|
|
1773
1862
|
}
|
|
1863
|
+
await reportNewDevice(context, signedIn);
|
|
1864
|
+
return { ...signedIn, recoveryCodesLeft: recoveryCodes.length };
|
|
1774
1865
|
}
|
|
1775
1866
|
|
|
1776
1867
|
// src/auth/second-factor/flows.ts
|
|
@@ -1779,12 +1870,12 @@ function secondFactorFlows(context, type, at) {
|
|
|
1779
1870
|
const api = {
|
|
1780
1871
|
...lifecycleFlows(context, type, at),
|
|
1781
1872
|
confirm: challenges.confirm,
|
|
1782
|
-
recover: (challenge, code) => recoverWithCode(context, type, challenge, code, at),
|
|
1873
|
+
recover: (challenge, code, options) => recoverWithCode(context, type, { challenge, code, options }, at),
|
|
1783
1874
|
recoveryCodesLeft: (user) => recoveryCodesLeftOf(context, type, user, at("secondFactor.recoveryCodesLeft"))
|
|
1784
1875
|
};
|
|
1785
1876
|
return {
|
|
1786
1877
|
api,
|
|
1787
|
-
finish: (record, where) => isActive(record.secondFactor) ? challenges.issue(record, where) : openSession(context, type, record)
|
|
1878
|
+
finish: (record, where, device) => isActive(record.secondFactor) ? challenges.issue(record, where) : openSession(context, type, record, device)
|
|
1788
1879
|
};
|
|
1789
1880
|
}
|
|
1790
1881
|
|
|
@@ -1794,8 +1885,8 @@ function signInCodeFlows(context, type, at, finish) {
|
|
|
1794
1885
|
async request(email) {
|
|
1795
1886
|
return requestCode(context, type, String(email));
|
|
1796
1887
|
},
|
|
1797
|
-
async confirm(challenge, code) {
|
|
1798
|
-
return confirmCode(context, type, finish, challenge, code, at);
|
|
1888
|
+
async confirm(challenge, code, options) {
|
|
1889
|
+
return confirmCode(context, type, finish, { challenge, code, options }, at);
|
|
1799
1890
|
}
|
|
1800
1891
|
};
|
|
1801
1892
|
}
|
|
@@ -1818,9 +1909,11 @@ async function requestCode(context, type, email) {
|
|
|
1818
1909
|
user: toUser(record)
|
|
1819
1910
|
};
|
|
1820
1911
|
}
|
|
1821
|
-
async function confirmCode(context, type, finish,
|
|
1912
|
+
async function confirmCode(context, type, finish, given, at) {
|
|
1822
1913
|
const where = at("signInCode.confirm");
|
|
1823
|
-
const
|
|
1914
|
+
const device = deviceHint(context, given.options, where);
|
|
1915
|
+
const secret = String(given.challenge);
|
|
1916
|
+
const code = given.code;
|
|
1824
1917
|
const { token, attemptsLeft } = await countCodeAttempt(context, secret, "signInCode", where, type.name);
|
|
1825
1918
|
const user = await findRecord(context, token.userId, type.name);
|
|
1826
1919
|
if (user === null) {
|
|
@@ -1837,7 +1930,8 @@ async function confirmCode(context, type, finish, challenge, code, at) {
|
|
|
1837
1930
|
user,
|
|
1838
1931
|
token,
|
|
1839
1932
|
where,
|
|
1840
|
-
noun: "code"
|
|
1933
|
+
noun: "code",
|
|
1934
|
+
device
|
|
1841
1935
|
});
|
|
1842
1936
|
}
|
|
1843
1937
|
|
|
@@ -2037,7 +2131,8 @@ async function insert(context, type, input, where) {
|
|
|
2037
2131
|
}
|
|
2038
2132
|
|
|
2039
2133
|
// src/auth/users/sign-in.ts
|
|
2040
|
-
async function signIn(context, type, finish, input,
|
|
2134
|
+
async function signIn(context, type, finish, input, call) {
|
|
2135
|
+
const { where, device } = call;
|
|
2041
2136
|
const rule = passwordRule(type, where);
|
|
2042
2137
|
const hasher = requireHasher(context, where);
|
|
2043
2138
|
const login = input?.[rule.login];
|
|
@@ -2065,27 +2160,31 @@ async function signIn(context, type, finish, input, where) {
|
|
|
2065
2160
|
await restartSignInCount(context, type, record, where);
|
|
2066
2161
|
}
|
|
2067
2162
|
const verified = await rehashed(context, record, String(password));
|
|
2068
|
-
const result = await finish(verified, where);
|
|
2163
|
+
const result = await finish(verified, where, device);
|
|
2069
2164
|
if (!await heldByPassword(context, type, verified, String(password), result, where)) {
|
|
2070
2165
|
throw refuse("wrongPassword");
|
|
2071
2166
|
}
|
|
2167
|
+
await reportNewDevice(context, result);
|
|
2072
2168
|
return result;
|
|
2073
2169
|
}
|
|
2074
2170
|
|
|
2075
2171
|
// src/auth/users/password-flows.ts
|
|
2076
2172
|
function passwordFlows(context, type, at, finish) {
|
|
2077
2173
|
return {
|
|
2078
|
-
async signUp(input) {
|
|
2174
|
+
async signUp(input, options) {
|
|
2079
2175
|
const where = at("signUp");
|
|
2176
|
+
const device = deviceHint(context, options, where);
|
|
2080
2177
|
passwordRule(type, where);
|
|
2081
2178
|
if (input?.password === undefined) {
|
|
2082
2179
|
checkPassword(type, undefined, where);
|
|
2083
2180
|
}
|
|
2084
2181
|
const record = await insert(context, type, input, where);
|
|
2085
|
-
return openSession(context, type, record);
|
|
2182
|
+
return openSession(context, type, record, device, true);
|
|
2086
2183
|
},
|
|
2087
|
-
async signIn(input) {
|
|
2088
|
-
|
|
2184
|
+
async signIn(input, options) {
|
|
2185
|
+
const where = at("signIn");
|
|
2186
|
+
const device = deviceHint(context, options, where);
|
|
2187
|
+
return signIn(context, type, finish, input, { where, device });
|
|
2089
2188
|
},
|
|
2090
2189
|
async findByLogin(login) {
|
|
2091
2190
|
const rule = passwordRule(type, at("findByLogin"));
|
|
@@ -2300,5 +2399,5 @@ export {
|
|
|
2300
2399
|
systemClock
|
|
2301
2400
|
};
|
|
2302
2401
|
|
|
2303
|
-
//# debugId=
|
|
2402
|
+
//# debugId=735288C311F315A564756E2164756E21
|
|
2304
2403
|
//# sourceMappingURL=index.js.map
|