@spine-event-engine/auth 2.0.0-snapshot.25 → 2.0.0-snapshot.28
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 +128 -11
- package/REFERENCE.md +166 -12
- package/dist/gateway/dynamic-unary-forwarder.d.ts +62 -3
- package/dist/gateway/dynamic-unary-forwarder.d.ts.map +1 -1
- package/dist/gateway/dynamic-unary-forwarder.js +189 -10
- package/dist/gateway/dynamic-unary-forwarder.js.map +1 -1
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +65 -7
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +60 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +43 -3
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +178 -32
- package/dist/native/index.js.map +1 -1
- package/dist/oidc/contracts.d.ts +18 -4
- package/dist/oidc/contracts.d.ts.map +1 -1
- package/dist/oidc/flow-store.d.ts +143 -0
- package/dist/oidc/flow-store.d.ts.map +1 -0
- package/dist/oidc/flow-store.js +184 -0
- package/dist/oidc/flow-store.js.map +1 -0
- package/dist/oidc/index.d.ts +29 -9
- package/dist/oidc/index.d.ts.map +1 -1
- package/dist/oidc/index.js +611 -152
- package/dist/oidc/index.js.map +1 -1
- package/dist/oidc/persistent-flow-store.d.ts +117 -0
- package/dist/oidc/persistent-flow-store.d.ts.map +1 -0
- package/dist/oidc/persistent-flow-store.js +413 -0
- package/dist/oidc/persistent-flow-store.js.map +1 -0
- package/dist/oidc/records.d.ts +15 -0
- package/dist/oidc/records.d.ts.map +1 -0
- package/dist/oidc/records.js +91 -0
- package/dist/oidc/records.js.map +1 -0
- package/dist/providers/index.d.ts +10 -5
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +103 -30
- package/dist/providers/index.js.map +1 -1
- package/dist/request/index.d.ts +3 -0
- package/dist/request/index.d.ts.map +1 -1
- package/dist/request/index.js +17 -1
- package/dist/request/index.js.map +1 -1
- package/dist/sessions/cookies.d.ts +46 -8
- package/dist/sessions/cookies.d.ts.map +1 -1
- package/dist/sessions/cookies.js +269 -47
- package/dist/sessions/cookies.js.map +1 -1
- package/dist/sessions/opaque-session-store.d.ts +143 -0
- package/dist/sessions/opaque-session-store.d.ts.map +1 -0
- package/dist/sessions/opaque-session-store.js +185 -0
- package/dist/sessions/opaque-session-store.js.map +1 -0
- package/dist/sessions/opaque.d.ts +93 -106
- package/dist/sessions/opaque.d.ts.map +1 -1
- package/dist/sessions/opaque.js +501 -232
- package/dist/sessions/opaque.js.map +1 -1
- package/dist/sessions/persistent-opaque-session-store.d.ts +105 -0
- package/dist/sessions/persistent-opaque-session-store.d.ts.map +1 -0
- package/dist/sessions/persistent-opaque-session-store.js +333 -0
- package/dist/sessions/persistent-opaque-session-store.js.map +1 -0
- package/dist/sessions/persistent-token-revocations.d.ts +90 -0
- package/dist/sessions/persistent-token-revocations.d.ts.map +1 -0
- package/dist/sessions/persistent-token-revocations.js +422 -0
- package/dist/sessions/persistent-token-revocations.js.map +1 -0
- package/dist/sessions/session-lifetime.d.ts +67 -0
- package/dist/sessions/session-lifetime.d.ts.map +1 -0
- package/dist/sessions/session-lifetime.js +165 -0
- package/dist/sessions/session-lifetime.js.map +1 -0
- package/dist/sessions/session-record.d.ts +15 -0
- package/dist/sessions/session-record.d.ts.map +1 -0
- package/dist/sessions/session-record.js +109 -0
- package/dist/sessions/session-record.js.map +1 -0
- package/dist/sessions/signed.d.ts +5 -4
- package/dist/sessions/signed.d.ts.map +1 -1
- package/dist/sessions/signed.js +212 -74
- package/dist/sessions/signed.js.map +1 -1
- package/dist/subscriptions/index.d.ts +35 -3
- package/dist/subscriptions/index.d.ts.map +1 -1
- package/dist/subscriptions/index.js +680 -134
- package/dist/subscriptions/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -6
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Timestamp } from "@bufbuild/protobuf/wkt";
|
|
2
|
+
import { type AuthenticatedPrincipal as ProtoPrincipal, type OpaqueSessionRecord } from "@spine-event-engine/proto/auth";
|
|
3
|
+
import type { AuthenticatedPrincipal } from "../index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Domain conversion and exact time ordering for opaque session records.
|
|
6
|
+
*/
|
|
7
|
+
export declare const SessionRecords: Readonly<{
|
|
8
|
+
deadline(value: Timestamp): Timestamp;
|
|
9
|
+
compare(left: Timestamp, right: Timestamp): number;
|
|
10
|
+
orderKey(value: Timestamp): Uint8Array;
|
|
11
|
+
principal(principal: AuthenticatedPrincipal): ProtoPrincipal;
|
|
12
|
+
publicPrincipal(record: OpaqueSessionRecord): AuthenticatedPrincipal;
|
|
13
|
+
expiry(record: OpaqueSessionRecord): Timestamp;
|
|
14
|
+
}>;
|
|
15
|
+
//# sourceMappingURL=session-record.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-record.d.ts","sourceRoot":"","sources":["../../src/sessions/session-record.ts"],"names":[],"mappings":"AAeA,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAGL,KAAK,sBAAsB,IAAI,cAAc,EAC7C,KAAK,mBAAmB,EACzB,MAAM,gCAAgC,CAAC;AAExC,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,OAAO,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAAC;IACvC,SAAS,CAAC,SAAS,EAAE,sBAAsB,GAAG,cAAc,CAAC;IAC7D,eAAe,CAAC,MAAM,EAAE,mBAAmB,GAAG,sBAAsB,CAAC;IACrE,MAAM,CAAC,MAAM,EAAE,mBAAmB,GAAG,SAAS,CAAC;CAChD,CA4FC,CAAC"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { clone, create } from "@bufbuild/protobuf";
|
|
15
|
+
import { TimestampSchema } from "@bufbuild/protobuf/wkt";
|
|
16
|
+
import { AuthenticatedPrincipalSchema, PrincipalAttributesSchema, } from "@spine-event-engine/proto/auth";
|
|
17
|
+
/**
|
|
18
|
+
* Domain conversion and exact time ordering for opaque session records.
|
|
19
|
+
*/
|
|
20
|
+
export const SessionRecords = Object.freeze({
|
|
21
|
+
/**
|
|
22
|
+
* Validates an exact Protobuf deadline.
|
|
23
|
+
*
|
|
24
|
+
* @param value Candidate deadline.
|
|
25
|
+
* @returns Detached validated deadline.
|
|
26
|
+
*/
|
|
27
|
+
deadline(value) {
|
|
28
|
+
if (value.seconds < -62135596800n ||
|
|
29
|
+
value.seconds > 253402300799n ||
|
|
30
|
+
!Number.isInteger(value.nanos) ||
|
|
31
|
+
value.nanos < 0 ||
|
|
32
|
+
value.nanos >= 1_000_000_000)
|
|
33
|
+
throw new RangeError("Session deadline is outside the Protobuf Timestamp range.");
|
|
34
|
+
return clone(TimestampSchema, value);
|
|
35
|
+
},
|
|
36
|
+
/**
|
|
37
|
+
* Compares exact deadlines without millisecond truncation.
|
|
38
|
+
*
|
|
39
|
+
* @param left First deadline.
|
|
40
|
+
* @param right Second deadline.
|
|
41
|
+
* @returns Negative, zero, or positive order.
|
|
42
|
+
*/
|
|
43
|
+
compare(left, right) {
|
|
44
|
+
if (left.seconds < right.seconds)
|
|
45
|
+
return -1;
|
|
46
|
+
if (left.seconds > right.seconds)
|
|
47
|
+
return 1;
|
|
48
|
+
return left.nanos - right.nanos;
|
|
49
|
+
},
|
|
50
|
+
/**
|
|
51
|
+
* Encodes a complete deadline for unsigned byte ordering.
|
|
52
|
+
*
|
|
53
|
+
* @param value Exact deadline.
|
|
54
|
+
* @returns Twelve ordered bytes.
|
|
55
|
+
*/
|
|
56
|
+
orderKey(value) {
|
|
57
|
+
const deadline = SessionRecords.deadline(value);
|
|
58
|
+
const bytes = Buffer.alloc(12);
|
|
59
|
+
bytes.writeBigUInt64BE(BigInt.asUintN(64, deadline.seconds) ^ 0x8000000000000000n, 0);
|
|
60
|
+
bytes.writeUInt32BE(deadline.nanos, 8);
|
|
61
|
+
return bytes;
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Copies a public principal into the domain message with attributes presence.
|
|
65
|
+
*
|
|
66
|
+
* @param principal Authenticated identity.
|
|
67
|
+
* @returns Complete Protobuf identity.
|
|
68
|
+
*/
|
|
69
|
+
principal(principal) {
|
|
70
|
+
if (typeof principal.id !== "string")
|
|
71
|
+
throw new TypeError("principal.id must be a string.");
|
|
72
|
+
if (principal.attributes === undefined)
|
|
73
|
+
return create(AuthenticatedPrincipalSchema, { id: principal.id });
|
|
74
|
+
const values = Object.fromEntries(Object.entries(principal.attributes));
|
|
75
|
+
if (Object.values(values).some((value) => typeof value !== "string"))
|
|
76
|
+
throw new TypeError("Principal attributes must be strings.");
|
|
77
|
+
return create(AuthenticatedPrincipalSchema, {
|
|
78
|
+
id: principal.id,
|
|
79
|
+
attributes: create(PrincipalAttributesSchema, { values }),
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
/**
|
|
83
|
+
* Copies a stored identity to the public principal shape.
|
|
84
|
+
*
|
|
85
|
+
* @param record Complete session record.
|
|
86
|
+
* @returns Detached public identity.
|
|
87
|
+
*/
|
|
88
|
+
publicPrincipal(record) {
|
|
89
|
+
const principal = record.principal;
|
|
90
|
+
if (principal === undefined)
|
|
91
|
+
throw new Error("Stored session has no principal.");
|
|
92
|
+
const attributes = principal.attributes;
|
|
93
|
+
return Object.freeze(attributes === undefined
|
|
94
|
+
? { id: principal.id }
|
|
95
|
+
: { id: principal.id, attributes: Object.freeze({ ...attributes.values }) });
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* Reads the required exact deadline from a stored session.
|
|
99
|
+
*
|
|
100
|
+
* @param record Complete session record.
|
|
101
|
+
* @returns Validated deadline.
|
|
102
|
+
*/
|
|
103
|
+
expiry(record) {
|
|
104
|
+
if (record.whenExpires === undefined)
|
|
105
|
+
throw new Error("Stored session has no expiry.");
|
|
106
|
+
return SessionRecords.deadline(record.whenExpires);
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
//# sourceMappingURL=session-record.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-record.js","sourceRoot":"","sources":["../../src/sessions/session-record.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAkB,MAAM,wBAAwB,CAAC;AACzE,OAAO,EACL,4BAA4B,EAC5B,yBAAyB,GAG1B,MAAM,gCAAgC,CAAC;AAIxC;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAOtB,MAAM,CAAC,MAAM,CAAC;IACjB;;;;;OAKG;IACH,QAAQ,CAAC,KAAgB;QACvB,IACE,KAAK,CAAC,OAAO,GAAG,CAAC,YAAe;YAChC,KAAK,CAAC,OAAO,GAAG,aAAgB;YAChC,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,CAAC;YAC9B,KAAK,CAAC,KAAK,GAAG,CAAC;YACf,KAAK,CAAC,KAAK,IAAI,aAAa;YAE5B,MAAM,IAAI,UAAU,CAAC,2DAA2D,CAAC,CAAC;QACpF,OAAO,KAAK,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;IACvC,CAAC;IAED;;;;;;OAMG;IACH,OAAO,CAAC,IAAe,EAAE,KAAgB;QACvC,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC,CAAC;QAC5C,IAAI,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO;YAAE,OAAO,CAAC,CAAC;QAC3C,OAAO,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAClC,CAAC;IAED;;;;;OAKG;IACH,QAAQ,CAAC,KAAgB;QACvB,MAAM,QAAQ,GAAG,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC/B,KAAK,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,mBAAsB,EAAE,CAAC,CAAC,CAAC;QACzF,KAAK,CAAC,aAAa,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACvC,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;;;;OAKG;IACH,SAAS,CAAC,SAAiC;QACzC,IAAI,OAAO,SAAS,CAAC,EAAE,KAAK,QAAQ;YAAE,MAAM,IAAI,SAAS,CAAC,gCAAgC,CAAC,CAAC;QAC5F,IAAI,SAAS,CAAC,UAAU,KAAK,SAAS;YACpC,OAAO,MAAM,CAAC,4BAA4B,EAAE,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC;QACpE,MAAM,MAAM,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC;QACxE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,CAAC;YAClE,MAAM,IAAI,SAAS,CAAC,uCAAuC,CAAC,CAAC;QAC/D,OAAO,MAAM,CAAC,4BAA4B,EAAE;YAC1C,EAAE,EAAE,SAAS,CAAC,EAAE;YAChB,UAAU,EAAE,MAAM,CAAC,yBAAyB,EAAE,EAAE,MAAM,EAAE,CAAC;SAC1D,CAAC,CAAC;IACL,CAAC;IAED;;;;;OAKG;IACH,eAAe,CAAC,MAA2B;QACzC,MAAM,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QACnC,IAAI,SAAS,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;QACjF,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC;QACxC,OAAO,MAAM,CAAC,MAAM,CAClB,UAAU,KAAK,SAAS;YACtB,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE;YACtB,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,CAC9E,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,MAAM,CAAC,MAA2B;QAChC,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS;YAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;QACvF,OAAO,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IACrD,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -59,10 +59,10 @@ export interface SignedTokenRevocation {
|
|
|
59
59
|
*/
|
|
60
60
|
isRevoked(jti: string): Promise<boolean>;
|
|
61
61
|
/**
|
|
62
|
-
* Stores the exact token ID through
|
|
63
|
-
* The
|
|
62
|
+
* Stores the exact token ID through the inclusive last acceptance instant.
|
|
63
|
+
* The strategy includes its configured clock tolerance before calling this port.
|
|
64
64
|
* @param jti The token identifier to retain.
|
|
65
|
-
* @param expiresAt The
|
|
65
|
+
* @param expiresAt The full last-acceptance deadline, including clock tolerance.
|
|
66
66
|
* @returns Completes after the token ID is retained.
|
|
67
67
|
*/
|
|
68
68
|
revoke(jti: string, expiresAt: Timestamp): Promise<void>;
|
|
@@ -214,9 +214,10 @@ export declare class SignedSessions implements SessionResolver {
|
|
|
214
214
|
* Cookie credentials and every malformed, invalid, expired, revoked, or
|
|
215
215
|
* unavailable-revocation result return `undefined`.
|
|
216
216
|
* @param credential The credential to resolve.
|
|
217
|
+
* @param lifetime Optional host operation; omission retains no monitor.
|
|
217
218
|
* @returns The verified session, or undefined when it cannot be trusted.
|
|
218
219
|
*/
|
|
219
|
-
resolve(credential: RequestCredential): Promise<ResolvedSession | undefined>;
|
|
220
|
+
resolve(credential: RequestCredential, lifetime?: AbortSignal): Promise<ResolvedSession | undefined>;
|
|
220
221
|
/**
|
|
221
222
|
* Updates the active P-256 key atomically.
|
|
222
223
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../src/sessions/signed.ts"],"names":[],"mappings":"AAgBA,OAAO,EACL,SAAS,
|
|
1
|
+
{"version":3,"file":"signed.d.ts","sourceRoot":"","sources":["../../src/sessions/signed.ts"],"names":[],"mappings":"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"}
|
package/dist/sessions/signed.js
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { Time } from "@spine-event-engine/core/time";
|
|
15
15
|
import { create } from "@bufbuild/protobuf";
|
|
16
|
-
import { KeyObject, createPrivateKey, createPublicKey, randomBytes as nodeRandomBytes,
|
|
16
|
+
import { KeyObject, createPrivateKey, createPublicKey, randomBytes as nodeRandomBytes, } from "node:crypto";
|
|
17
17
|
import { TimestampSchema } from "@bufbuild/protobuf/wkt";
|
|
18
|
+
import { SignJWT, decodeProtectedHeader, jwtVerify } from "jose";
|
|
19
|
+
import { SessionLifetimes } from "./session-lifetime.js";
|
|
18
20
|
/**
|
|
19
21
|
* Finite, local ES256/JWT application sessions. This class has no remote key
|
|
20
22
|
* discovery, persistence, OIDC behavior, or authorization policy. It imports
|
|
@@ -36,6 +38,10 @@ export class SignedSessions {
|
|
|
36
38
|
#keys = new Map();
|
|
37
39
|
#revocation;
|
|
38
40
|
#closed = false;
|
|
41
|
+
#lifetimes = new SessionLifetimes(() => {
|
|
42
|
+
const now = this.#now();
|
|
43
|
+
return typeof now === "number" ? now : undefined;
|
|
44
|
+
});
|
|
39
45
|
/**
|
|
40
46
|
* Creates signed sessions with copied finite configuration and P-256 keys.
|
|
41
47
|
* @param options The issuer, key ring, limits, clock, entropy, and revocation settings.
|
|
@@ -71,80 +77,156 @@ export class SignedSessions {
|
|
|
71
77
|
* @param principal The authenticated principal to encode.
|
|
72
78
|
* @returns The issuance outcome and, when accepted, credential and session.
|
|
73
79
|
*/
|
|
74
|
-
issue(principal) {
|
|
80
|
+
async issue(principal) {
|
|
75
81
|
if (this.#isClosed())
|
|
76
|
-
return
|
|
82
|
+
return SignedSessionValues.rejected("closed");
|
|
77
83
|
const now = this.#now();
|
|
78
84
|
if (typeof now !== "number")
|
|
79
|
-
return
|
|
85
|
+
return SignedSessionValues.rejected(now.kind === "closed" ? "closed" : "clock-failure");
|
|
80
86
|
if (this.#isClosed())
|
|
81
|
-
return
|
|
87
|
+
return SignedSessionValues.rejected("closed");
|
|
82
88
|
this.#sweep(now);
|
|
89
|
+
return this.#issuePrepared(principal);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Copies the principal and constructs bounded claims before signing.
|
|
93
|
+
*
|
|
94
|
+
* @param principal Authenticated principal supplied for issuance.
|
|
95
|
+
* @returns Issuance outcome after signing or a specific input failure.
|
|
96
|
+
*/
|
|
97
|
+
async #issuePrepared(principal) {
|
|
83
98
|
let copied;
|
|
84
99
|
try {
|
|
85
100
|
copied = SignedSessionValues.principalCopy(principal, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
|
|
86
101
|
}
|
|
87
102
|
catch {
|
|
88
|
-
return
|
|
103
|
+
return SignedSessionValues.rejected("principal-invalid");
|
|
89
104
|
}
|
|
90
105
|
if (copied === undefined)
|
|
91
|
-
return
|
|
106
|
+
return SignedSessionValues.rejected("principal-invalid");
|
|
92
107
|
const jti = this.#jti();
|
|
93
108
|
if (jti === undefined)
|
|
94
|
-
return
|
|
109
|
+
return this.#isClosed()
|
|
95
110
|
? SignedSessionValues.rejected("closed")
|
|
96
|
-
: SignedSessionValues.rejected("entropy-failure")
|
|
111
|
+
: SignedSessionValues.rejected("entropy-failure");
|
|
97
112
|
if (this.#isClosed())
|
|
98
|
-
return
|
|
113
|
+
return SignedSessionValues.rejected("closed");
|
|
99
114
|
const again = this.#now();
|
|
100
115
|
if (typeof again !== "number")
|
|
101
|
-
return
|
|
116
|
+
return SignedSessionValues.rejected(again.kind === "closed" ? "closed" : "clock-failure");
|
|
102
117
|
if (this.#isClosed())
|
|
103
|
-
return
|
|
104
|
-
const
|
|
118
|
+
return SignedSessionValues.rejected("closed");
|
|
119
|
+
const claims = this.#issueClaims(copied, jti, again);
|
|
120
|
+
if (claims === undefined)
|
|
121
|
+
return SignedSessionValues.rejected("clock-failure");
|
|
122
|
+
return this.#signIssued(claims, copied);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Builds exact signed claims when their expiry fits the trusted clock domain.
|
|
126
|
+
*
|
|
127
|
+
* @param principal Detached authenticated principal.
|
|
128
|
+
* @param jti Fresh token identifier.
|
|
129
|
+
* @param now Trusted issuance time in milliseconds.
|
|
130
|
+
* @returns Claims, or undefined when expiry exceeds the Timestamp range.
|
|
131
|
+
*/
|
|
132
|
+
#issueClaims(principal, jti, now) {
|
|
133
|
+
const iat = Math.floor(now / 1000);
|
|
105
134
|
const exp = iat + this.#ttl;
|
|
106
135
|
if (!Number.isSafeInteger(exp) || !SignedSessionValues.timeValid(exp * 1000))
|
|
107
|
-
return
|
|
108
|
-
|
|
136
|
+
return undefined;
|
|
137
|
+
return {
|
|
109
138
|
iss: this.#issuer,
|
|
110
139
|
aud: this.#audience,
|
|
111
|
-
sub:
|
|
140
|
+
sub: principal.id,
|
|
112
141
|
iat,
|
|
113
142
|
nbf: iat,
|
|
114
143
|
exp,
|
|
115
144
|
jti,
|
|
116
|
-
...(
|
|
145
|
+
...(principal.attributes === undefined ? {} : { attributes: principal.attributes }),
|
|
117
146
|
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Signs claims and rechecks closure and time after the awaited signature.
|
|
150
|
+
*
|
|
151
|
+
* @param claims Validated claims to sign.
|
|
152
|
+
* @param principal Detached authenticated principal.
|
|
153
|
+
* @returns Issued credential or a failure without exposing a late token.
|
|
154
|
+
*/
|
|
155
|
+
async #signIssued(claims, principal) {
|
|
118
156
|
try {
|
|
119
|
-
const token = this.#token(claims);
|
|
157
|
+
const token = await this.#token(claims);
|
|
120
158
|
if (token.length > this.#maxToken)
|
|
121
|
-
return
|
|
159
|
+
return SignedSessionValues.rejected("signing-failure");
|
|
122
160
|
if (this.#isClosed())
|
|
123
|
-
return
|
|
124
|
-
|
|
161
|
+
return SignedSessionValues.rejected("closed");
|
|
162
|
+
if (!this.#signedTimeValid(claims))
|
|
163
|
+
return SignedSessionValues.rejected("clock-failure");
|
|
164
|
+
if (this.#isClosed())
|
|
165
|
+
return SignedSessionValues.rejected("closed");
|
|
166
|
+
return {
|
|
125
167
|
kind: "issued",
|
|
126
168
|
credential: Object.freeze({ kind: "bearer", value: token }),
|
|
127
|
-
session: SignedSessionValues.session(
|
|
128
|
-
}
|
|
169
|
+
session: SignedSessionValues.session(principal, claims.exp),
|
|
170
|
+
};
|
|
129
171
|
}
|
|
130
172
|
catch {
|
|
131
|
-
return
|
|
173
|
+
return this.#isClosed()
|
|
174
|
+
? SignedSessionValues.rejected("closed")
|
|
175
|
+
: SignedSessionValues.rejected("signing-failure");
|
|
132
176
|
}
|
|
133
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Checks issued claims against a fresh trusted clock reading.
|
|
180
|
+
*
|
|
181
|
+
* @param claims Claims signed by the active key.
|
|
182
|
+
* @returns Whether they still satisfy the exact application time policy.
|
|
183
|
+
*/
|
|
184
|
+
#signedTimeValid(claims) {
|
|
185
|
+
const signedAt = this.#now();
|
|
186
|
+
return (typeof signedAt === "number" &&
|
|
187
|
+
SignedSessionValues.validClaims(claims, this.#issuer, this.#audience, this.#ttl, this.#skew, signedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars) !== undefined);
|
|
188
|
+
}
|
|
134
189
|
/**
|
|
135
190
|
* Resolves one locally verifiable bearer credential.
|
|
136
191
|
*
|
|
137
192
|
* Cookie credentials and every malformed, invalid, expired, revoked, or
|
|
138
193
|
* unavailable-revocation result return `undefined`.
|
|
139
194
|
* @param credential The credential to resolve.
|
|
195
|
+
* @param lifetime Optional host operation; omission retains no monitor.
|
|
140
196
|
* @returns The verified session, or undefined when it cannot be trusted.
|
|
141
197
|
*/
|
|
142
|
-
async resolve(credential) {
|
|
198
|
+
async resolve(credential, lifetime) {
|
|
143
199
|
if (this.#isClosed() || credential.kind !== "bearer")
|
|
144
200
|
return undefined;
|
|
145
|
-
|
|
146
|
-
|
|
201
|
+
lifetime?.throwIfAborted();
|
|
202
|
+
const claims = await this.#verifiedClaims(credential.value);
|
|
203
|
+
if (claims === undefined || this.#isClosed())
|
|
147
204
|
return undefined;
|
|
205
|
+
const monitor = lifetime === undefined
|
|
206
|
+
? undefined
|
|
207
|
+
: this.#lifetimes.register(this.#identity(claims.jti), lifetime);
|
|
208
|
+
let retained = false;
|
|
209
|
+
try {
|
|
210
|
+
const resolved = await this.#resolveClaims(claims);
|
|
211
|
+
if (resolved === undefined || monitor === undefined)
|
|
212
|
+
return resolved;
|
|
213
|
+
if (!monitor.ready(resolved.expiresAt))
|
|
214
|
+
return undefined;
|
|
215
|
+
retained = true;
|
|
216
|
+
return { ...resolved, invalidated: monitor.signal };
|
|
217
|
+
}
|
|
218
|
+
finally {
|
|
219
|
+
if (!retained)
|
|
220
|
+
monitor?.release();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Checks authoritative revocation and time after monitored registration.
|
|
225
|
+
*
|
|
226
|
+
* @param claims Verified token claims.
|
|
227
|
+
* @returns Detached session if still valid.
|
|
228
|
+
*/
|
|
229
|
+
async #resolveClaims(claims) {
|
|
148
230
|
if (this.#revocation !== undefined)
|
|
149
231
|
try {
|
|
150
232
|
if (await this.#revocation.isRevoked(claims.jti))
|
|
@@ -153,13 +235,25 @@ export class SignedSessions {
|
|
|
153
235
|
catch {
|
|
154
236
|
return undefined;
|
|
155
237
|
}
|
|
156
|
-
|
|
238
|
+
const checkedAt = this.#now();
|
|
239
|
+
if (typeof checkedAt !== "number" || this.#isClosed())
|
|
240
|
+
return undefined;
|
|
241
|
+
if (SignedSessionValues.validClaims(claims, this.#issuer, this.#audience, this.#ttl, this.#skew, checkedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars) === undefined)
|
|
157
242
|
return undefined;
|
|
158
243
|
return SignedSessionValues.session({
|
|
159
244
|
id: claims.sub,
|
|
160
245
|
...(claims.attributes === undefined ? {} : { attributes: claims.attributes }),
|
|
161
246
|
}, claims.exp);
|
|
162
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Scopes a token ID to this strategy's issuer and audience.
|
|
250
|
+
*
|
|
251
|
+
* @param jti Authenticated token ID.
|
|
252
|
+
* @returns Local monitor key.
|
|
253
|
+
*/
|
|
254
|
+
#identity(jti) {
|
|
255
|
+
return `${this.#issuer}\u0000${this.#audience}\u0000${jti}`;
|
|
256
|
+
}
|
|
163
257
|
/**
|
|
164
258
|
* Updates the active P-256 key atomically.
|
|
165
259
|
*
|
|
@@ -223,15 +317,20 @@ export class SignedSessions {
|
|
|
223
317
|
const resolved = await this.#claimsForLogout(credential);
|
|
224
318
|
if (resolved === undefined)
|
|
225
319
|
return { kind: "expiryOnly" };
|
|
226
|
-
if (this.#revocation === undefined)
|
|
320
|
+
if (this.#revocation === undefined) {
|
|
321
|
+
this.#lifetimes.invalidate(this.#identity(resolved.jti));
|
|
227
322
|
return { kind: "expiryOnly" };
|
|
323
|
+
}
|
|
228
324
|
try {
|
|
229
|
-
await this.#revocation.revoke(resolved.jti, SignedSessionValues.
|
|
325
|
+
await this.#revocation.revoke(resolved.jti, SignedSessionValues.lastAcceptance(resolved.exp, this.#skew));
|
|
230
326
|
return this.#closed ? { kind: "expiryOnly" } : { kind: "revoked" };
|
|
231
327
|
}
|
|
232
328
|
catch {
|
|
233
329
|
return this.#closed ? { kind: "expiryOnly" } : { kind: "unavailable" };
|
|
234
330
|
}
|
|
331
|
+
finally {
|
|
332
|
+
this.#lifetimes.invalidate(this.#identity(resolved.jti));
|
|
333
|
+
}
|
|
235
334
|
}
|
|
236
335
|
/**
|
|
237
336
|
* Closes the resolver and clears active, verification-key, and revocation references.
|
|
@@ -240,6 +339,7 @@ export class SignedSessions {
|
|
|
240
339
|
*/
|
|
241
340
|
close() {
|
|
242
341
|
this.#closed = true;
|
|
342
|
+
this.#lifetimes.close();
|
|
243
343
|
this.#keys.clear();
|
|
244
344
|
this.#active = undefined;
|
|
245
345
|
this.#revocation = undefined;
|
|
@@ -248,9 +348,15 @@ export class SignedSessions {
|
|
|
248
348
|
#claimsForLogout(credential) {
|
|
249
349
|
if (credential.kind !== "bearer" || this.#isClosed())
|
|
250
350
|
return Promise.resolve(undefined);
|
|
251
|
-
return
|
|
351
|
+
return this.#verifiedClaims(credential.value);
|
|
252
352
|
}
|
|
253
|
-
|
|
353
|
+
/**
|
|
354
|
+
* Verifies a compact token and checks current key and claim policy again.
|
|
355
|
+
*
|
|
356
|
+
* @param value Compact bearer token.
|
|
357
|
+
* @returns Trusted claims, or undefined on any verification failure.
|
|
358
|
+
*/
|
|
359
|
+
async #verifiedClaims(value) {
|
|
254
360
|
const parsed = this.#parse(value);
|
|
255
361
|
if (parsed === undefined)
|
|
256
362
|
return undefined;
|
|
@@ -261,29 +367,63 @@ export class SignedSessions {
|
|
|
261
367
|
const key = this.#keys.get(parsed.kid);
|
|
262
368
|
if (key === undefined || (key.expiresAt !== undefined && now > key.expiresAt))
|
|
263
369
|
return undefined;
|
|
370
|
+
const verified = await this.#verifyToken(value, key.publicKey, now);
|
|
371
|
+
if (verified === undefined)
|
|
372
|
+
return undefined;
|
|
373
|
+
const checkedAt = this.#now();
|
|
374
|
+
if (typeof checkedAt !== "number" || this.#isClosed())
|
|
375
|
+
return undefined;
|
|
376
|
+
this.#sweep(checkedAt);
|
|
377
|
+
if (!this.#keys.has(parsed.kid) || (key.expiresAt !== undefined && checkedAt > key.expiresAt))
|
|
378
|
+
return undefined;
|
|
379
|
+
return SignedSessionValues.validClaims(verified, this.#issuer, this.#audience, this.#ttl, this.#skew, checkedAt, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Runs jose JWT verification with preliminary time tolerance.
|
|
383
|
+
*
|
|
384
|
+
* @param value Compact bearer token.
|
|
385
|
+
* @param key Selected public verification key.
|
|
386
|
+
* @param now Trusted time sampled before verification.
|
|
387
|
+
* @returns Verified payload, or undefined when jose rejects the token.
|
|
388
|
+
*/
|
|
389
|
+
async #verifyToken(value, key, now) {
|
|
264
390
|
try {
|
|
265
|
-
|
|
266
|
-
|
|
391
|
+
const result = await jwtVerify(value, key, {
|
|
392
|
+
algorithms: ["ES256"],
|
|
393
|
+
typ: "JWT",
|
|
394
|
+
issuer: this.#issuer,
|
|
395
|
+
audience: this.#audience,
|
|
396
|
+
requiredClaims: ["sub", "iat", "nbf", "exp", "jti"],
|
|
397
|
+
maxTokenAge: this.#ttl,
|
|
398
|
+
clockTolerance: this.#skew + 1,
|
|
399
|
+
currentDate: new Date(now),
|
|
400
|
+
});
|
|
401
|
+
return result.payload;
|
|
267
402
|
}
|
|
268
403
|
catch {
|
|
269
404
|
return undefined;
|
|
270
405
|
}
|
|
271
|
-
return SignedSessionValues.validClaims(parsed.claims, this.#issuer, this.#audience, this.#ttl, this.#skew, now, this.#maxId, this.#maxAttributes, this.#maxAttributeChars);
|
|
272
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Signs the exact JWT header and claims with the active P-256 key.
|
|
409
|
+
*
|
|
410
|
+
* @param claims Validated application claims.
|
|
411
|
+
* @returns Compact ES256 JWT from jose.
|
|
412
|
+
*/
|
|
273
413
|
#token(claims) {
|
|
274
414
|
const active = this.#active;
|
|
275
415
|
if (active === undefined)
|
|
276
416
|
throw new Error("SignedSessions is closed");
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
417
|
+
return new SignJWT(claims.attributes === undefined ? {} : { attributes: claims.attributes })
|
|
418
|
+
.setProtectedHeader({ alg: "ES256", typ: "JWT", kid: active.kid })
|
|
419
|
+
.setIssuer(claims.iss)
|
|
420
|
+
.setAudience(claims.aud)
|
|
421
|
+
.setSubject(claims.sub)
|
|
422
|
+
.setIssuedAt(claims.iat)
|
|
423
|
+
.setNotBefore(claims.nbf)
|
|
424
|
+
.setExpirationTime(claims.exp)
|
|
425
|
+
.setJti(claims.jti)
|
|
426
|
+
.sign(active.privateKey);
|
|
287
427
|
}
|
|
288
428
|
#jti() {
|
|
289
429
|
let bytes;
|
|
@@ -322,6 +462,12 @@ export class SignedSessions {
|
|
|
322
462
|
if (key.expiresAt !== undefined && now > key.expiresAt)
|
|
323
463
|
this.#keys.delete(kid);
|
|
324
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Checks compact encoding bounds and reads the exact protected header with jose.
|
|
467
|
+
*
|
|
468
|
+
* @param value Candidate compact bearer token.
|
|
469
|
+
* @returns Validated key ID, or undefined for an unsupported encoding or header.
|
|
470
|
+
*/
|
|
325
471
|
#parse(value) {
|
|
326
472
|
if (typeof value !== "string" || value.length === 0 || value.length > this.#maxToken)
|
|
327
473
|
return undefined;
|
|
@@ -331,9 +477,11 @@ export class SignedSessions {
|
|
|
331
477
|
return undefined;
|
|
332
478
|
const [encodedHeader, encodedClaims, encodedSignature] = segments;
|
|
333
479
|
try {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
480
|
+
if (Buffer.byteLength(encodedHeader, "base64url") > 16_384 ||
|
|
481
|
+
Buffer.byteLength(encodedClaims, "base64url") > 16_384)
|
|
482
|
+
return undefined;
|
|
483
|
+
const header = decodeProtectedHeader(value);
|
|
484
|
+
if (header.alg !== "ES256" ||
|
|
337
485
|
header.typ !== "JWT" ||
|
|
338
486
|
typeof header.kid !== "string" ||
|
|
339
487
|
Object.keys(header).length !== 3 ||
|
|
@@ -342,12 +490,7 @@ export class SignedSessions {
|
|
|
342
490
|
const signature = Buffer.from(encodedSignature, "base64url");
|
|
343
491
|
if (signature.byteLength !== 64 || signature.toString("base64url") !== encodedSignature)
|
|
344
492
|
return undefined;
|
|
345
|
-
return {
|
|
346
|
-
kid: header.kid,
|
|
347
|
-
input: `${encodedHeader}.${encodedClaims}`,
|
|
348
|
-
signature,
|
|
349
|
-
claims: SignedSessionValues.json(encodedClaims),
|
|
350
|
-
};
|
|
493
|
+
return { kid: header.kid };
|
|
351
494
|
}
|
|
352
495
|
catch {
|
|
353
496
|
return undefined;
|
|
@@ -357,6 +500,7 @@ export class SignedSessions {
|
|
|
357
500
|
/**
|
|
358
501
|
* Validates, serializes, and constructs finite signed-session values.
|
|
359
502
|
*/
|
|
503
|
+
const maxTimestampSecond = 253402300799n;
|
|
360
504
|
const SignedSessionValues = Object.freeze({
|
|
361
505
|
/**
|
|
362
506
|
* Validates the issuer, audience, and finite token limits before key installation.
|
|
@@ -498,25 +642,6 @@ const SignedSessionValues = Object.freeze({
|
|
|
498
642
|
base64url(value) {
|
|
499
643
|
return value.length > 0 && /^[A-Za-z0-9_-]+$/.test(value) && value.length % 4 !== 1;
|
|
500
644
|
},
|
|
501
|
-
/**
|
|
502
|
-
* Encodes a JSON value as URL-safe base64.
|
|
503
|
-
* @param value The JSON value to encode.
|
|
504
|
-
* @returns The encoded JSON string.
|
|
505
|
-
*/
|
|
506
|
-
encode(value) {
|
|
507
|
-
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
508
|
-
},
|
|
509
|
-
/**
|
|
510
|
-
* Decodes bounded URL-safe base64 JSON.
|
|
511
|
-
* @param value The URL-safe base64 JSON string to decode.
|
|
512
|
-
* @returns The decoded JSON value.
|
|
513
|
-
*/
|
|
514
|
-
json(value) {
|
|
515
|
-
const decoded = Buffer.from(value, "base64url");
|
|
516
|
-
if (decoded.byteLength > 16_384)
|
|
517
|
-
throw new Error();
|
|
518
|
-
return JSON.parse(decoded.toString("utf8"));
|
|
519
|
-
},
|
|
520
645
|
/**
|
|
521
646
|
* Checks whether a value is a plain object.
|
|
522
647
|
* @param value The unknown value to test for plain-record shape.
|
|
@@ -619,6 +744,19 @@ const SignedSessionValues = Object.freeze({
|
|
|
619
744
|
timestamp(seconds) {
|
|
620
745
|
return create(TimestampSchema, { seconds: BigInt(seconds) });
|
|
621
746
|
},
|
|
747
|
+
/**
|
|
748
|
+
* Calculates the final representable acceptance instant without unsafe addition.
|
|
749
|
+
*
|
|
750
|
+
* @param expiry Signed token expiry in epoch seconds.
|
|
751
|
+
* @param skew Accepted clock tolerance in seconds.
|
|
752
|
+
* @returns Exact inclusive deadline within the configured clock's Timestamp domain.
|
|
753
|
+
*/
|
|
754
|
+
lastAcceptance(expiry, skew) {
|
|
755
|
+
const endpoint = BigInt(expiry) + BigInt(skew);
|
|
756
|
+
return endpoint > maxTimestampSecond
|
|
757
|
+
? create(TimestampSchema, { seconds: maxTimestampSecond, nanos: 999_999_999 })
|
|
758
|
+
: create(TimestampSchema, { seconds: endpoint });
|
|
759
|
+
},
|
|
622
760
|
/**
|
|
623
761
|
* Creates a resolved session with a detached principal.
|
|
624
762
|
* @param principal The principal to copy.
|
|
@@ -641,7 +779,7 @@ const SignedSessionValues = Object.freeze({
|
|
|
641
779
|
*/
|
|
642
780
|
timeValid(milliseconds) {
|
|
643
781
|
const seconds = Math.floor(milliseconds / 1000);
|
|
644
|
-
return seconds >= -62_135_596_800 && seconds <=
|
|
782
|
+
return seconds >= -62_135_596_800 && seconds <= Number(maxTimestampSecond);
|
|
645
783
|
},
|
|
646
784
|
/**
|
|
647
785
|
* Checks a token identifier against expected random encoding.
|