@spfn/auth 0.2.0-beta.86 → 0.2.0-beta.87
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 +14 -9
- package/dist/{authenticate-DTA7W4v2.d.ts → authenticate-C7w-pLOR.d.ts} +60 -5
- package/dist/client-proof.d.ts +185 -46
- package/dist/client-proof.js +223 -34
- package/dist/client-proof.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/server.d.ts +91 -70
- package/dist/server.js +878 -69
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
package/dist/client-proof.js
CHANGED
|
@@ -345,7 +345,7 @@ function encodeString(value) {
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
// src/server/client-proof/proof.ts
|
|
348
|
-
import { createHash,
|
|
348
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify } from "crypto";
|
|
349
349
|
var CLIENT_PROOF_PROFILE = "clientProofV1";
|
|
350
350
|
var ABSENT_BODY_SHA256 = "0".repeat(64);
|
|
351
351
|
var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
|
|
@@ -360,6 +360,10 @@ var PROOF_INPUT_FIELDS = [
|
|
|
360
360
|
"bodySha256"
|
|
361
361
|
];
|
|
362
362
|
var PROOF_INPUT_SEPARATOR = "\n";
|
|
363
|
+
var PROOF_SIGNATURE_BYTES = 64;
|
|
364
|
+
var PROOF_SIGNATURE_HEX_LENGTH = PROOF_SIGNATURE_BYTES * 2;
|
|
365
|
+
var PROOF_SIGNATURE_PATTERN = /^[0-9a-f]{128}$/;
|
|
366
|
+
var RAW_SIGNATURE_ENCODING = "ieee-p1363";
|
|
363
367
|
var ProofInputError = class extends Error {
|
|
364
368
|
constructor() {
|
|
365
369
|
super("proof input field contains a C0 control character");
|
|
@@ -387,19 +391,43 @@ function canonicalProofInput(input) {
|
|
|
387
391
|
}
|
|
388
392
|
return fields.join(PROOF_INPUT_SEPARATOR);
|
|
389
393
|
}
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
function parseClientProofPublicKey(spkiDerBase64) {
|
|
395
|
+
const key = createPublicKey({
|
|
396
|
+
key: Buffer.from(spkiDerBase64, "base64"),
|
|
397
|
+
format: "der",
|
|
398
|
+
type: "spki"
|
|
399
|
+
});
|
|
400
|
+
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
401
|
+
throw new Error("a clientProofV1 public key must be an ECDSA P-256 key");
|
|
402
|
+
}
|
|
403
|
+
return key;
|
|
395
404
|
}
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
if (a.length !== b.length) {
|
|
405
|
+
function verifyClientProof(input, presentedProof, publicKey) {
|
|
406
|
+
const data = Buffer.from(canonicalProofInput(input), "utf8");
|
|
407
|
+
if (!PROOF_SIGNATURE_PATTERN.test(presentedProof)) {
|
|
400
408
|
return false;
|
|
401
409
|
}
|
|
402
|
-
return
|
|
410
|
+
return verify(
|
|
411
|
+
"sha256",
|
|
412
|
+
data,
|
|
413
|
+
{ key: publicKey, dsaEncoding: RAW_SIGNATURE_ENCODING },
|
|
414
|
+
Buffer.from(presentedProof, "hex")
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
function signClientProof(input, privateKeyPkcs8DerBase64) {
|
|
418
|
+
const key = createPrivateKey({
|
|
419
|
+
key: Buffer.from(privateKeyPkcs8DerBase64, "base64"),
|
|
420
|
+
format: "der",
|
|
421
|
+
type: "pkcs8"
|
|
422
|
+
});
|
|
423
|
+
return sign(
|
|
424
|
+
"sha256",
|
|
425
|
+
Buffer.from(canonicalProofInput(input), "utf8"),
|
|
426
|
+
{ key, dsaEncoding: RAW_SIGNATURE_ENCODING }
|
|
427
|
+
).toString("hex");
|
|
428
|
+
}
|
|
429
|
+
function sha256Hex(bytes) {
|
|
430
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
403
431
|
}
|
|
404
432
|
|
|
405
433
|
// src/server/client-proof/refusal.ts
|
|
@@ -490,6 +518,86 @@ function contractViolation(message) {
|
|
|
490
518
|
return new ClientProofRefusal("CONTRACT_UNSUPPORTED", message);
|
|
491
519
|
}
|
|
492
520
|
|
|
521
|
+
// src/server/client-proof/replay-store.ts
|
|
522
|
+
import { getCache } from "@spfn/core/cache";
|
|
523
|
+
function replayLedgerKey(clientId, nonce) {
|
|
524
|
+
return JSON.stringify([clientId, nonce]);
|
|
525
|
+
}
|
|
526
|
+
var MemoryReplayLedger = class {
|
|
527
|
+
/** replayLedgerKey(...) → the millis it was spent at. */
|
|
528
|
+
spent = /* @__PURE__ */ new Map();
|
|
529
|
+
isSpent(clientId, nonce) {
|
|
530
|
+
return this.spent.has(replayLedgerKey(clientId, nonce));
|
|
531
|
+
}
|
|
532
|
+
/** Records the pair at `atMillis`; false when it was already spent. */
|
|
533
|
+
spend(clientId, nonce, atMillis) {
|
|
534
|
+
const key = replayLedgerKey(clientId, nonce);
|
|
535
|
+
if (this.spent.has(key)) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
this.spent.set(key, atMillis);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
/** Drops entries older than the window, judged against `nowMillis`. */
|
|
542
|
+
prune(nowMillis, windowMillis) {
|
|
543
|
+
for (const [key, spentAtMillis] of this.spent) {
|
|
544
|
+
if (nowMillis - spentAtMillis > windowMillis) {
|
|
545
|
+
this.spent.delete(key);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
get size() {
|
|
550
|
+
return this.spent.size;
|
|
551
|
+
}
|
|
552
|
+
clear() {
|
|
553
|
+
this.spent.clear();
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
var MemoryReplayStore = class {
|
|
557
|
+
constructor(windowMillis = DEFAULT_REPLAY_WINDOW_MILLIS) {
|
|
558
|
+
this.windowMillis = windowMillis;
|
|
559
|
+
}
|
|
560
|
+
ledger = new MemoryReplayLedger();
|
|
561
|
+
async isSpent(clientId, nonce) {
|
|
562
|
+
this.ledger.prune(Date.now(), this.windowMillis);
|
|
563
|
+
return this.ledger.isSpent(clientId, nonce);
|
|
564
|
+
}
|
|
565
|
+
async spend(clientId, nonce) {
|
|
566
|
+
const now = Date.now();
|
|
567
|
+
this.ledger.prune(now, this.windowMillis);
|
|
568
|
+
return this.ledger.spend(clientId, nonce, now);
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
var RedisReplayStore = class {
|
|
572
|
+
constructor(windowMillis = DEFAULT_REPLAY_WINDOW_MILLIS) {
|
|
573
|
+
this.windowMillis = windowMillis;
|
|
574
|
+
}
|
|
575
|
+
async isSpent(clientId, nonce) {
|
|
576
|
+
return await this.cache().exists(this.key(clientId, nonce)) === 1;
|
|
577
|
+
}
|
|
578
|
+
async spend(clientId, nonce) {
|
|
579
|
+
return await this.cache().set(this.key(clientId, nonce), "1", "PX", this.windowMillis, "NX") === "OK";
|
|
580
|
+
}
|
|
581
|
+
cache() {
|
|
582
|
+
const cache = getCache();
|
|
583
|
+
if (!cache) {
|
|
584
|
+
throw new Error("client-proof replay ledger: cache is not available");
|
|
585
|
+
}
|
|
586
|
+
return cache;
|
|
587
|
+
}
|
|
588
|
+
key(clientId, nonce) {
|
|
589
|
+
return `spfn:auth:client-proof:replay:${sha256Hex(Buffer.from(replayLedgerKey(clientId, nonce), "utf8"))}`;
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
var configured = null;
|
|
593
|
+
function configureClientProofReplayStore(store) {
|
|
594
|
+
configured = store;
|
|
595
|
+
}
|
|
596
|
+
function getClientProofReplayStore() {
|
|
597
|
+
configured ??= new MemoryReplayStore();
|
|
598
|
+
return configured;
|
|
599
|
+
}
|
|
600
|
+
|
|
493
601
|
// src/server/client-proof/state.ts
|
|
494
602
|
function systemClock() {
|
|
495
603
|
return { nowMillis: () => Date.now() };
|
|
@@ -506,16 +614,14 @@ var TestClock = class {
|
|
|
506
614
|
}
|
|
507
615
|
};
|
|
508
616
|
var DEFAULT_SESSION_TTL_MILLIS = 6e5;
|
|
509
|
-
function replayKeyOf(clientId, nonce) {
|
|
510
|
-
return `${clientId}${nonce}`;
|
|
511
|
-
}
|
|
512
617
|
var ClientProofState = class {
|
|
513
618
|
replayWindowMillis;
|
|
514
619
|
clock;
|
|
515
|
-
|
|
620
|
+
initialPublicKeys;
|
|
621
|
+
publicKeys = /* @__PURE__ */ new Map();
|
|
516
622
|
sessions = /* @__PURE__ */ new Map();
|
|
517
|
-
/**
|
|
518
|
-
spentNonces =
|
|
623
|
+
/** The replay ledger — the shared memory implementation, used dev-only here. */
|
|
624
|
+
spentNonces = new MemoryReplayLedger();
|
|
519
625
|
revokedKeyIds = /* @__PURE__ */ new Set();
|
|
520
626
|
holds = /* @__PURE__ */ new Map();
|
|
521
627
|
initialSessionTtlMillis;
|
|
@@ -530,10 +636,22 @@ var ClientProofState = class {
|
|
|
530
636
|
this.initialSessionTtlMillis = options.sessionTtlMillis ?? DEFAULT_SESSION_TTL_MILLIS;
|
|
531
637
|
this.sessionTtlMillis = this.initialSessionTtlMillis;
|
|
532
638
|
this.replayWindowMillis = options.replayWindowMillis ?? DEFAULT_REPLAY_WINDOW_MILLIS;
|
|
533
|
-
|
|
534
|
-
|
|
639
|
+
this.initialPublicKeys = new Map(
|
|
640
|
+
Object.entries(options.publicKeys).map(([keyId, spki]) => [keyId, parseClientProofPublicKey(spki)])
|
|
641
|
+
);
|
|
642
|
+
for (const [keyId, key] of this.initialPublicKeys) {
|
|
643
|
+
this.publicKeys.set(keyId, key);
|
|
535
644
|
}
|
|
536
645
|
}
|
|
646
|
+
// ---- key registration --------------------------------------------------
|
|
647
|
+
/**
|
|
648
|
+
* Registers (or replaces) the public key `keyId` presents proofs under.
|
|
649
|
+
*
|
|
650
|
+
* @throws when the key is not base64 SPKI DER naming a P-256 key.
|
|
651
|
+
*/
|
|
652
|
+
registerPublicKey(keyId, publicKeySpkiDerBase64) {
|
|
653
|
+
this.publicKeys.set(keyId, parseClientProofPublicKey(publicKeySpkiDerBase64));
|
|
654
|
+
}
|
|
537
655
|
// ---- admission ---------------------------------------------------------
|
|
538
656
|
/**
|
|
539
657
|
* Runs the contract's checks in the contract's order and returns the
|
|
@@ -555,18 +673,17 @@ var ClientProofState = class {
|
|
|
555
673
|
if (age < 0 || age > this.replayWindowMillis) {
|
|
556
674
|
return ClientProofRefusal.proofExpired();
|
|
557
675
|
}
|
|
558
|
-
|
|
559
|
-
if (this.spentNonces.has(replayKey)) {
|
|
676
|
+
if (this.spentNonces.isSpent(args.clientId, args.proofInput.nonce)) {
|
|
560
677
|
return ClientProofRefusal.proofReplayed();
|
|
561
678
|
}
|
|
562
|
-
const
|
|
563
|
-
if (
|
|
679
|
+
const publicKey = this.publicKeys.get(args.keyId);
|
|
680
|
+
if (publicKey === void 0) {
|
|
564
681
|
return ClientProofRefusal.proofInvalid();
|
|
565
682
|
}
|
|
566
|
-
if (!
|
|
683
|
+
if (!verifyClientProof(args.proofInput, args.presentedProof, publicKey)) {
|
|
567
684
|
return ClientProofRefusal.proofInvalid();
|
|
568
685
|
}
|
|
569
|
-
this.spentNonces.
|
|
686
|
+
this.spentNonces.spend(args.clientId, args.proofInput.nonce, Number(args.proofInput.issuedAtMillis));
|
|
570
687
|
return null;
|
|
571
688
|
}
|
|
572
689
|
// ---- sessions ----------------------------------------------------------
|
|
@@ -599,8 +716,12 @@ var ClientProofState = class {
|
|
|
599
716
|
setSessionTtlMillis(millis) {
|
|
600
717
|
this.sessionTtlMillis = millis;
|
|
601
718
|
}
|
|
602
|
-
/** Returns the state to how it started, counters included. */
|
|
719
|
+
/** Returns the state to how it started, counters and registered keys included. */
|
|
603
720
|
reset() {
|
|
721
|
+
this.publicKeys.clear();
|
|
722
|
+
for (const [keyId, key] of this.initialPublicKeys) {
|
|
723
|
+
this.publicKeys.set(keyId, key);
|
|
724
|
+
}
|
|
604
725
|
this.sessions.clear();
|
|
605
726
|
this.spentNonces.clear();
|
|
606
727
|
this.revokedKeyIds.clear();
|
|
@@ -678,11 +799,7 @@ var ClientProofState = class {
|
|
|
678
799
|
this.sessions.delete(sessionId);
|
|
679
800
|
}
|
|
680
801
|
}
|
|
681
|
-
|
|
682
|
-
if (nowMillis - issuedAtMillis > this.replayWindowMillis) {
|
|
683
|
-
this.spentNonces.delete(key);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
802
|
+
this.spentNonces.prune(nowMillis, this.replayWindowMillis);
|
|
686
803
|
}
|
|
687
804
|
};
|
|
688
805
|
|
|
@@ -826,6 +943,48 @@ var CONTRACT_OPERATIONS = [
|
|
|
826
943
|
summary: "Authenticated paged read covering optional fields and arrays."
|
|
827
944
|
}
|
|
828
945
|
];
|
|
946
|
+
var AUTH_SURFACE_OPERATIONS = [
|
|
947
|
+
{
|
|
948
|
+
id: "auth.enroll.register",
|
|
949
|
+
method: "POST",
|
|
950
|
+
path: "/_auth/register",
|
|
951
|
+
authProfile: "none",
|
|
952
|
+
requiresSession: false,
|
|
953
|
+
requestType: "RegisterRequest",
|
|
954
|
+
responseType: "RegisterResponse",
|
|
955
|
+
summary: "Registers an account with a verification token and enrolls the client-generated public key."
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
id: "auth.enroll.login",
|
|
959
|
+
method: "POST",
|
|
960
|
+
path: "/_auth/login",
|
|
961
|
+
authProfile: "none",
|
|
962
|
+
requiresSession: false,
|
|
963
|
+
requestType: "LoginRequest",
|
|
964
|
+
responseType: "LoginResponse",
|
|
965
|
+
summary: "Authenticates with password credentials and enrolls a fresh client-generated public key."
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
id: "auth.enroll.oauthNative",
|
|
969
|
+
method: "POST",
|
|
970
|
+
path: "/_auth/oauth/{provider}/native",
|
|
971
|
+
authProfile: "none",
|
|
972
|
+
requiresSession: false,
|
|
973
|
+
requestType: "OauthNativeRequest",
|
|
974
|
+
responseType: "OauthNativeResponse",
|
|
975
|
+
summary: "Verifies a native/web social id_token server-side and enrolls the client-generated public key."
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
id: "auth.keys.rotate",
|
|
979
|
+
method: "POST",
|
|
980
|
+
path: "/_auth/keys/rotate",
|
|
981
|
+
authProfile: "clientProofV1",
|
|
982
|
+
requiresSession: false,
|
|
983
|
+
requestType: "RotateKeyRequest",
|
|
984
|
+
responseType: "RotateKeyResponse",
|
|
985
|
+
summary: "Replaces the authenticated key with a new client-generated public key before its TTL runs out."
|
|
986
|
+
}
|
|
987
|
+
];
|
|
829
988
|
var ContractTypeError = class extends Error {
|
|
830
989
|
constructor() {
|
|
831
990
|
super("not the declared contract type");
|
|
@@ -937,6 +1096,8 @@ async function handleControlRequest(state, controlToken, path, request) {
|
|
|
937
1096
|
case "/control/expire-sessions":
|
|
938
1097
|
state.expireSessions();
|
|
939
1098
|
return ok();
|
|
1099
|
+
case "/control/register-key":
|
|
1100
|
+
return registerKey(state, body);
|
|
940
1101
|
case "/control/revoke-key":
|
|
941
1102
|
return revokeKey(state, body);
|
|
942
1103
|
case "/control/session-ttl":
|
|
@@ -961,6 +1122,22 @@ function stats(state) {
|
|
|
961
1122
|
["spentNonceCount", BigInt(counters.spentNonceCount)]
|
|
962
1123
|
])));
|
|
963
1124
|
}
|
|
1125
|
+
function registerKey(state, body) {
|
|
1126
|
+
const keyId = stringField(body, "keyId");
|
|
1127
|
+
const publicKey = stringField(body, "publicKey");
|
|
1128
|
+
if (keyId === null) {
|
|
1129
|
+
return badRequest("keyId");
|
|
1130
|
+
}
|
|
1131
|
+
if (publicKey === null) {
|
|
1132
|
+
return badRequest("publicKey");
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
state.registerPublicKey(keyId, publicKey);
|
|
1136
|
+
} catch {
|
|
1137
|
+
return badRequest("publicKey");
|
|
1138
|
+
}
|
|
1139
|
+
return ok();
|
|
1140
|
+
}
|
|
964
1141
|
function revokeKey(state, body) {
|
|
965
1142
|
const keyId = stringField(body, "keyId");
|
|
966
1143
|
if (keyId === null) {
|
|
@@ -1226,6 +1403,7 @@ function createClientProofGuard(state, options = {}) {
|
|
|
1226
1403
|
}
|
|
1227
1404
|
export {
|
|
1228
1405
|
ABSENT_BODY_SHA256,
|
|
1406
|
+
AUTH_SURFACE_OPERATIONS,
|
|
1229
1407
|
CLIENT_PROOF_CONTENT_TYPE,
|
|
1230
1408
|
CLIENT_PROOF_HEADERS,
|
|
1231
1409
|
CLIENT_PROOF_PROFILE,
|
|
@@ -1240,12 +1418,16 @@ export {
|
|
|
1240
1418
|
DEFAULT_SESSION_TTL_MILLIS,
|
|
1241
1419
|
DEV_CATALOGUE,
|
|
1242
1420
|
DEV_MAX_LIMIT,
|
|
1421
|
+
MemoryReplayLedger,
|
|
1422
|
+
MemoryReplayStore,
|
|
1423
|
+
PROOF_SIGNATURE_BYTES,
|
|
1424
|
+
PROOF_SIGNATURE_HEX_LENGTH,
|
|
1243
1425
|
ProofInputError,
|
|
1426
|
+
RedisReplayStore,
|
|
1244
1427
|
TestClock,
|
|
1245
1428
|
admitClientProofRequest,
|
|
1246
1429
|
canonicalProofInput,
|
|
1247
|
-
|
|
1248
|
-
constantTimeEqualsProof,
|
|
1430
|
+
configureClientProofReplayStore,
|
|
1249
1431
|
createClientProofDevHandler,
|
|
1250
1432
|
createClientProofGuard,
|
|
1251
1433
|
decodeEchoRequest,
|
|
@@ -1255,10 +1437,17 @@ export {
|
|
|
1255
1437
|
encodeEchoResponse,
|
|
1256
1438
|
encodeHandshakeResponse,
|
|
1257
1439
|
encodeListItemsResponse,
|
|
1440
|
+
getClientProofReplayStore,
|
|
1258
1441
|
isCanonicalBytes,
|
|
1442
|
+
isRequestContentType,
|
|
1259
1443
|
newHexId,
|
|
1260
1444
|
parseCanonicalJson,
|
|
1445
|
+
parseClientProofPublicKey,
|
|
1446
|
+
readCredentials,
|
|
1447
|
+
replayLedgerKey,
|
|
1261
1448
|
sha256Hex,
|
|
1262
|
-
|
|
1449
|
+
signClientProof,
|
|
1450
|
+
systemClock,
|
|
1451
|
+
verifyClientProof
|
|
1263
1452
|
};
|
|
1264
1453
|
//# sourceMappingURL=client-proof.js.map
|