@saurbit/oauth2-jwt 0.1.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/LICENSE +21 -0
- package/README.md +152 -0
- package/esm/jose_jwks_authority.d.ts +90 -0
- package/esm/jose_jwks_authority.d.ts.map +1 -0
- package/esm/jose_jwks_authority.js +214 -0
- package/esm/jwks_key_store.d.ts +53 -0
- package/esm/jwks_key_store.d.ts.map +1 -0
- package/esm/jwks_key_store.js +86 -0
- package/esm/jwks_rotator.d.ts +36 -0
- package/esm/jwks_rotator.d.ts.map +1 -0
- package/esm/jwks_rotator.js +54 -0
- package/esm/methods.d.ts +39 -0
- package/esm/methods.d.ts.map +1 -0
- package/esm/methods.js +52 -0
- package/esm/mod.d.ts +6 -0
- package/esm/mod.d.ts.map +1 -0
- package/esm/mod.js +4 -0
- package/esm/package.json +3 -0
- package/esm/types.d.ts +179 -0
- package/esm/types.d.ts.map +1 -0
- package/esm/types.js +1 -0
- package/package.json +33 -0
- package/script/jose_jwks_authority.d.ts +90 -0
- package/script/jose_jwks_authority.d.ts.map +1 -0
- package/script/jose_jwks_authority.js +218 -0
- package/script/jwks_key_store.d.ts +53 -0
- package/script/jwks_key_store.d.ts.map +1 -0
- package/script/jwks_key_store.js +91 -0
- package/script/jwks_rotator.d.ts +36 -0
- package/script/jwks_rotator.d.ts.map +1 -0
- package/script/jwks_rotator.js +58 -0
- package/script/methods.d.ts +39 -0
- package/script/methods.d.ts.map +1 -0
- package/script/methods.js +58 -0
- package/script/mod.d.ts +6 -0
- package/script/mod.d.ts.map +1 -0
- package/script/mod.js +14 -0
- package/script/package.json +3 -0
- package/script/types.d.ts +179 -0
- package/script/types.d.ts.map +1 -0
- package/script/types.js +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 saurbit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# @saurbit/oauth2-jwt
|
|
2
|
+
|
|
3
|
+
JWT utilities and JWKS authority for [`@saurbit/oauth2`](https://jsr.io/@saurbit/oauth2). Wraps
|
|
4
|
+
[jose](https://github.com/panva/jose) to provide ready-made implementations of the JWT-related
|
|
5
|
+
interfaces required by `@saurbit/oauth2`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
**Node.js / Bun**
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @saurbit/oauth2-jwt
|
|
13
|
+
# or
|
|
14
|
+
yarn add @saurbit/oauth2-jwt
|
|
15
|
+
# or
|
|
16
|
+
pnpm add @saurbit/oauth2-jwt
|
|
17
|
+
# or
|
|
18
|
+
bun add @saurbit/oauth2-jwt
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Deno / JSR**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
deno add jsr:@saurbit/oauth2-jwt
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## JWT Utilities
|
|
28
|
+
|
|
29
|
+
`@saurbit/oauth2-jwt` provides three ready-made functions that satisfy the `JwtVerify`, `JwtDecode`,
|
|
30
|
+
and `JwkVerify` interfaces expected by `@saurbit/oauth2`.
|
|
31
|
+
|
|
32
|
+
### `verifyJwt` and `decodeJwt`
|
|
33
|
+
|
|
34
|
+
Used by the `ClientSecretJwt` and `PrivateKeyJwt` client authentication methods.
|
|
35
|
+
|
|
36
|
+
**`ClientSecretJwt`**
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { ClientSecretJwt } from "@saurbit/oauth2";
|
|
40
|
+
import { decodeJwt, verifyJwt } from "@saurbit/oauth2-jwt";
|
|
41
|
+
|
|
42
|
+
const clientSecretJwt = new ClientSecretJwt(decodeJwt, verifyJwt);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**`PrivateKeyJwt`**
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { PrivateKeyJwt } from "@saurbit/oauth2";
|
|
49
|
+
import { decodeJwt, verifyJwt } from "@saurbit/oauth2-jwt";
|
|
50
|
+
|
|
51
|
+
const privateKeyJwt = new PrivateKeyJwt(decodeJwt, verifyJwt);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### `verifyJwk`
|
|
55
|
+
|
|
56
|
+
Used by `DPoPTokenType` for Demonstration of Proof-of-Possession token validation.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { createInMemoryReplayStore, DPoPTokenType } from "@saurbit/oauth2";
|
|
60
|
+
import { verifyJwk } from "@saurbit/oauth2-jwt";
|
|
61
|
+
|
|
62
|
+
const dpop = new DPoPTokenType(verifyJwk, createInMemoryReplayStore());
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## JWKS Authority
|
|
66
|
+
|
|
67
|
+
`JoseJwksAuthority` manages RS256 signing key pairs, signs and verifies JWTs, and returns the JWKS
|
|
68
|
+
endpoint payload. Keys are stored in a `JwksKeyStore` and generated automatically on first use.
|
|
69
|
+
|
|
70
|
+
### Setup
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { createInMemoryKeyStore, JoseJwksAuthority } from "@saurbit/oauth2-jwt";
|
|
74
|
+
|
|
75
|
+
const store = createInMemoryKeyStore();
|
|
76
|
+
const authority = new JoseJwksAuthority(store, 8.64e+6); // public keys valid for 100 days
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Sign a JWT
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
const { token } = await authority.sign({
|
|
83
|
+
sub: "user-123",
|
|
84
|
+
iss: "https://auth.example.com",
|
|
85
|
+
aud: "my-client",
|
|
86
|
+
iat: Math.floor(Date.now() / 1000),
|
|
87
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
88
|
+
jti: crypto.randomUUID(),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Verify a JWT
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const payload = await authority.verify(token);
|
|
96
|
+
console.log(payload.sub); // "user-123"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### JWKS endpoint
|
|
100
|
+
|
|
101
|
+
`getJwksEndpointResponse()` returns the set of current public keys in JWKS format, ready to be
|
|
102
|
+
served at a well-known endpoint (e.g. `/.well-known/jwks.json`).
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// Example with Hono
|
|
106
|
+
app.get("/jwks", async (c) => {
|
|
107
|
+
return c.json(await authority.getJwksEndpointResponse());
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The response has the shape `{ keys: RawKey[] }` and is safe to return directly as JSON.
|
|
112
|
+
|
|
113
|
+
## Key Rotation
|
|
114
|
+
|
|
115
|
+
`JwksRotator` manages scheduled key rotation. It compares the current time against the last rotation
|
|
116
|
+
timestamp and generates a new key pair only when the configured interval has elapsed.
|
|
117
|
+
|
|
118
|
+
Call `checkAndRotateKeys()` at service startup and/or on a recurring schedule (e.g. every hour). If
|
|
119
|
+
a rotation is due a new key pair is generated; otherwise the call is a no-op. During rotation the
|
|
120
|
+
previous public key remains available in the JWKS until its TTL expires, so in-flight tokens
|
|
121
|
+
continue to verify correctly.
|
|
122
|
+
|
|
123
|
+
### Setup
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { createInMemoryKeyStore, JoseJwksAuthority, JwksRotator } from "@saurbit/oauth2-jwt";
|
|
127
|
+
|
|
128
|
+
const store = createInMemoryKeyStore();
|
|
129
|
+
const authority = new JoseJwksAuthority(store, 8.64e+6); // 100 days key TTL
|
|
130
|
+
|
|
131
|
+
const rotator = new JwksRotator({
|
|
132
|
+
keyGenerator: authority,
|
|
133
|
+
rotatorKeyStore: store,
|
|
134
|
+
rotationIntervalMs: 7.884e9, // 91 days
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Usage
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// At startup
|
|
142
|
+
await rotator.checkAndRotateKeys();
|
|
143
|
+
|
|
144
|
+
// Or on a recurring schedule
|
|
145
|
+
setInterval(async () => {
|
|
146
|
+
await rotator.checkAndRotateKeys();
|
|
147
|
+
}, 60 * 60 * 1000); // every hour
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { JWTPayload } from "jose";
|
|
2
|
+
import { JwksKeyStore, JwtAuthority, RawKey, RSA } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* JWT authority backed by [jose](https://github.com/panva/jose).
|
|
5
|
+
*
|
|
6
|
+
* Manages RS256 signing key pairs, signs and verifies JWTs, and returns the
|
|
7
|
+
* JWKS endpoint payload. Keys are stored in a {@link JwksKeyStore} and are
|
|
8
|
+
* generated automatically on first use.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { createInMemoryKeyStore, JoseJwksAuthority } from "@saurbit/oauth2-jwt";
|
|
13
|
+
*
|
|
14
|
+
* const store = createInMemoryKeyStore();
|
|
15
|
+
* const authority = new JoseJwksAuthority(store, 86400); // keys valid for 24 h
|
|
16
|
+
*
|
|
17
|
+
* const { token } = await authority.sign({ sub: "user-123", exp: Math.floor(Date.now() / 1000) + 3600 });
|
|
18
|
+
* const payload = await authority.verify(token);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare class JoseJwksAuthority implements JwtAuthority {
|
|
22
|
+
#private;
|
|
23
|
+
/**
|
|
24
|
+
* @param store The key store used to manage JWKS keys.
|
|
25
|
+
* @param ttl Time-to-live for the keys in seconds.
|
|
26
|
+
*/
|
|
27
|
+
constructor(store: JwksKeyStore, ttl?: number);
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves the current set of public keys from the key store.
|
|
30
|
+
* @returns An object containing the array of public keys.
|
|
31
|
+
*/
|
|
32
|
+
getPublicKeys(): Promise<{
|
|
33
|
+
keys: RawKey[];
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Get current kid for observability/debugging
|
|
37
|
+
*/
|
|
38
|
+
getCurrentKid(): Promise<string | undefined>;
|
|
39
|
+
/**
|
|
40
|
+
* Helps implement the JWKS endpoint by returning the current set of public keys in the expected format.
|
|
41
|
+
* @returns The current set of public keys in the JWKS format.
|
|
42
|
+
*/
|
|
43
|
+
getJwksEndpointResponse(): Promise<{
|
|
44
|
+
keys: RawKey[];
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Retrieves the public key corresponding to the given "kid" from the JWKS.
|
|
48
|
+
* @param kid The key ID of the desired public key.
|
|
49
|
+
* @returns The RSA public key if found, otherwise undefined.
|
|
50
|
+
*/
|
|
51
|
+
getPublicKey(kid: string): Promise<RSA | undefined>;
|
|
52
|
+
/**
|
|
53
|
+
* Generates a new RSA key pair, stores it in the key store with the appropriate metadata
|
|
54
|
+
* (kid, alg, use), and ensures that the public key is available for JWKS exposure.
|
|
55
|
+
*/
|
|
56
|
+
generateKeyPair(): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Signs the given payload as a JWT using the current private key.
|
|
59
|
+
* The "kid" of the signing key is included in the JWT header to allow verifiers
|
|
60
|
+
* to select the correct public key from the JWKS for verification.
|
|
61
|
+
* This method ensures that the JWT is signed with a valid key and that
|
|
62
|
+
* the corresponding public key is available in the JWKS for future verification.
|
|
63
|
+
* @param payload The payload to be signed as a JWT.
|
|
64
|
+
* @returns An object containing the signed JWT and the key ID used for signing.
|
|
65
|
+
*/
|
|
66
|
+
sign(payload: JWTPayload): Promise<{
|
|
67
|
+
token: string;
|
|
68
|
+
kid: string;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Verifies the given JWT using the appropriate public key from the JWKS based on the "kid" in the JWT header.
|
|
72
|
+
* It performs additional checks to ensure the integrity and expected structure of the JWT,
|
|
73
|
+
* such as validating the algorithm and ensuring no unexpected JWK is present in the header.
|
|
74
|
+
* @param token
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
verify<P extends JWTPayload = JWTPayload>(token: string): Promise<P>;
|
|
78
|
+
/**
|
|
79
|
+
* Signs multiple payloads with the same private key, returning an array of tokens and their corresponding kids.
|
|
80
|
+
* This is useful for batch operations where multiple JWTs need to be issued at once,
|
|
81
|
+
* ensuring they all use the same key and can be verified against the same public key in the JWKS.
|
|
82
|
+
* @param payloads The array of payloads to be signed as JWTs.
|
|
83
|
+
* @returns An array of objects containing the signed JWTs and their corresponding key IDs.
|
|
84
|
+
*/
|
|
85
|
+
signMany(payloads: JWTPayload[]): Promise<{
|
|
86
|
+
token: string;
|
|
87
|
+
kid: string;
|
|
88
|
+
}[]>;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=jose_jwks_authority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jose_jwks_authority.d.ts","sourceRoot":"","sources":["../src/jose_jwks_authority.ts"],"names":[],"mappings":"AAAA,OAAO,EAML,UAAU,EAGX,MAAM,MAAM,CAAC;AACd,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AA0BrE;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,iBAAkB,YAAW,YAAY;;IAOpD;;;OAGG;gBACS,KAAK,EAAE,YAAY,EAAE,GAAG,GAAE,MAAc;IAsCpD;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IASlD;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAKlD;;;OAGG;IACH,uBAAuB,IAAI,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAItD;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,GAAG,SAAS,CAAC;IAQzD;;;OAGG;IACG,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;;;;;;;OAQG;IACG,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAoBxE;;;;;;OAMG;IACG,MAAM,CAAC,CAAC,SAAS,UAAU,GAAG,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IA+B1E;;;;;;OAMG;IACG,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAyBlF"}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _JoseJwksAuthority_instances, _JoseJwksAuthority_store, _JoseJwksAuthority_ttl, _JoseJwksAuthority_generateAndStoreKeyPair, _JoseJwksAuthority_getPrivateKey;
|
|
13
|
+
import { exportJWK, generateKeyPair, importJWK, jwtVerify, SignJWT, } from "jose";
|
|
14
|
+
function base64UrlToString(b64url) {
|
|
15
|
+
// Fast path for Node/Bun
|
|
16
|
+
if (typeof Buffer !== "undefined") {
|
|
17
|
+
return Buffer.from(b64url, "base64url").toString();
|
|
18
|
+
}
|
|
19
|
+
// Convert Base64URL → standard Base64
|
|
20
|
+
const base64 = b64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
21
|
+
// Add padding if missing
|
|
22
|
+
const padded = base64.padEnd(base64.length + (4 - (base64.length % 4)) % 4, "=");
|
|
23
|
+
// Decode
|
|
24
|
+
const binary = atob(padded);
|
|
25
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
26
|
+
return new TextDecoder().decode(bytes);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* JWT authority backed by [jose](https://github.com/panva/jose).
|
|
30
|
+
*
|
|
31
|
+
* Manages RS256 signing key pairs, signs and verifies JWTs, and returns the
|
|
32
|
+
* JWKS endpoint payload. Keys are stored in a {@link JwksKeyStore} and are
|
|
33
|
+
* generated automatically on first use.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { createInMemoryKeyStore, JoseJwksAuthority } from "@saurbit/oauth2-jwt";
|
|
38
|
+
*
|
|
39
|
+
* const store = createInMemoryKeyStore();
|
|
40
|
+
* const authority = new JoseJwksAuthority(store, 86400); // keys valid for 24 h
|
|
41
|
+
*
|
|
42
|
+
* const { token } = await authority.sign({ sub: "user-123", exp: Math.floor(Date.now() / 1000) + 3600 });
|
|
43
|
+
* const payload = await authority.verify(token);
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export class JoseJwksAuthority {
|
|
47
|
+
/**
|
|
48
|
+
* @param store The key store used to manage JWKS keys.
|
|
49
|
+
* @param ttl Time-to-live for the keys in seconds.
|
|
50
|
+
*/
|
|
51
|
+
constructor(store, ttl = 36000) {
|
|
52
|
+
_JoseJwksAuthority_instances.add(this);
|
|
53
|
+
_JoseJwksAuthority_store.set(this, void 0);
|
|
54
|
+
/**
|
|
55
|
+
* seconds
|
|
56
|
+
*/
|
|
57
|
+
_JoseJwksAuthority_ttl.set(this, void 0);
|
|
58
|
+
__classPrivateFieldSet(this, _JoseJwksAuthority_store, store, "f");
|
|
59
|
+
__classPrivateFieldSet(this, _JoseJwksAuthority_ttl, ttl, "f");
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves the current set of public keys from the key store.
|
|
63
|
+
* @returns An object containing the array of public keys.
|
|
64
|
+
*/
|
|
65
|
+
async getPublicKeys() {
|
|
66
|
+
const publicJwks = await __classPrivateFieldGet(this, _JoseJwksAuthority_store, "f").getPublicKeys();
|
|
67
|
+
const json = { keys: publicJwks };
|
|
68
|
+
if (json && "keys" in json && Array.isArray(json.keys)) {
|
|
69
|
+
json.keys = [...json.keys].reverse();
|
|
70
|
+
}
|
|
71
|
+
return json;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Get current kid for observability/debugging
|
|
75
|
+
*/
|
|
76
|
+
async getCurrentKid() {
|
|
77
|
+
const key = await __classPrivateFieldGet(this, _JoseJwksAuthority_instances, "m", _JoseJwksAuthority_getPrivateKey).call(this);
|
|
78
|
+
return key?.kid;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Helps implement the JWKS endpoint by returning the current set of public keys in the expected format.
|
|
82
|
+
* @returns The current set of public keys in the JWKS format.
|
|
83
|
+
*/
|
|
84
|
+
getJwksEndpointResponse() {
|
|
85
|
+
return this.getPublicKeys();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Retrieves the public key corresponding to the given "kid" from the JWKS.
|
|
89
|
+
* @param kid The key ID of the desired public key.
|
|
90
|
+
* @returns The RSA public key if found, otherwise undefined.
|
|
91
|
+
*/
|
|
92
|
+
async getPublicKey(kid) {
|
|
93
|
+
const keyStore = await this.getPublicKeys();
|
|
94
|
+
const key = keyStore.keys.find((k) => k.kid === kid);
|
|
95
|
+
if (!key)
|
|
96
|
+
return undefined;
|
|
97
|
+
if (key.kty !== "RSA")
|
|
98
|
+
return undefined;
|
|
99
|
+
return key;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Generates a new RSA key pair, stores it in the key store with the appropriate metadata
|
|
103
|
+
* (kid, alg, use), and ensures that the public key is available for JWKS exposure.
|
|
104
|
+
*/
|
|
105
|
+
async generateKeyPair() {
|
|
106
|
+
await __classPrivateFieldGet(this, _JoseJwksAuthority_instances, "m", _JoseJwksAuthority_generateAndStoreKeyPair).call(this);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Signs the given payload as a JWT using the current private key.
|
|
110
|
+
* The "kid" of the signing key is included in the JWT header to allow verifiers
|
|
111
|
+
* to select the correct public key from the JWKS for verification.
|
|
112
|
+
* This method ensures that the JWT is signed with a valid key and that
|
|
113
|
+
* the corresponding public key is available in the JWKS for future verification.
|
|
114
|
+
* @param payload The payload to be signed as a JWT.
|
|
115
|
+
* @returns An object containing the signed JWT and the key ID used for signing.
|
|
116
|
+
*/
|
|
117
|
+
async sign(payload) {
|
|
118
|
+
const key = await __classPrivateFieldGet(this, _JoseJwksAuthority_instances, "m", _JoseJwksAuthority_getPrivateKey).call(this);
|
|
119
|
+
if (key.kty !== "RSA") {
|
|
120
|
+
throw new Error("Invalid key type, expected RSA");
|
|
121
|
+
}
|
|
122
|
+
if (!key.kid) {
|
|
123
|
+
throw new Error('Key is missing "kid" property');
|
|
124
|
+
}
|
|
125
|
+
const privateKey = await importJWK(key, "RS256");
|
|
126
|
+
const token = await new SignJWT(payload)
|
|
127
|
+
.setProtectedHeader({ typ: "jwt", alg: "RS256", kid: key.kid })
|
|
128
|
+
.sign(privateKey);
|
|
129
|
+
return { token, kid: key.kid };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Verifies the given JWT using the appropriate public key from the JWKS based on the "kid" in the JWT header.
|
|
133
|
+
* It performs additional checks to ensure the integrity and expected structure of the JWT,
|
|
134
|
+
* such as validating the algorithm and ensuring no unexpected JWK is present in the header.
|
|
135
|
+
* @param token
|
|
136
|
+
* @returns
|
|
137
|
+
*/
|
|
138
|
+
async verify(token) {
|
|
139
|
+
const [header] = token.split(".");
|
|
140
|
+
if (!header)
|
|
141
|
+
throw new Error("Invalid JWT format");
|
|
142
|
+
const parsedHeader = JSON.parse(base64UrlToString(header));
|
|
143
|
+
const kid = parsedHeader.kid;
|
|
144
|
+
if (!kid || typeof kid !== "string")
|
|
145
|
+
throw new Error('Invalid or missing "kid" in JWT header');
|
|
146
|
+
const key = await this.getPublicKey(kid);
|
|
147
|
+
if (!key)
|
|
148
|
+
throw new Error(`Key with kid "${kid}" not found`);
|
|
149
|
+
const { payload, protectedHeader } = await jwtVerify(token, key);
|
|
150
|
+
if (protectedHeader.alg !== "RS256") {
|
|
151
|
+
throw new Error(`Unexpected algorithm: ${protectedHeader.alg}`);
|
|
152
|
+
}
|
|
153
|
+
// additional checks hardening security
|
|
154
|
+
if ("jwk" in protectedHeader) {
|
|
155
|
+
throw new Error("Unexpected JWK in header - potential forgery attempt");
|
|
156
|
+
}
|
|
157
|
+
if (protectedHeader.typ && protectedHeader.typ.toLowerCase() !== "jwt") {
|
|
158
|
+
throw new Error(`Unexpected typ: ${protectedHeader.typ}`);
|
|
159
|
+
}
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Signs multiple payloads with the same private key, returning an array of tokens and their corresponding kids.
|
|
164
|
+
* This is useful for batch operations where multiple JWTs need to be issued at once,
|
|
165
|
+
* ensuring they all use the same key and can be verified against the same public key in the JWKS.
|
|
166
|
+
* @param payloads The array of payloads to be signed as JWTs.
|
|
167
|
+
* @returns An array of objects containing the signed JWTs and their corresponding key IDs.
|
|
168
|
+
*/
|
|
169
|
+
async signMany(payloads) {
|
|
170
|
+
const key = await __classPrivateFieldGet(this, _JoseJwksAuthority_instances, "m", _JoseJwksAuthority_getPrivateKey).call(this);
|
|
171
|
+
if (key.kty !== "RSA") {
|
|
172
|
+
throw new Error("Invalid key type, expected RSA");
|
|
173
|
+
}
|
|
174
|
+
if (!key.kid) {
|
|
175
|
+
throw new Error('Key is missing "kid" property');
|
|
176
|
+
}
|
|
177
|
+
const privateKey = await importJWK(key, "RS256");
|
|
178
|
+
const result = [];
|
|
179
|
+
for (const payload of payloads) {
|
|
180
|
+
const token = await new SignJWT(payload)
|
|
181
|
+
.setProtectedHeader({ typ: "jwt", alg: "RS256", kid: key.kid })
|
|
182
|
+
.sign(privateKey);
|
|
183
|
+
result.push({ token, kid: key.kid });
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
_JoseJwksAuthority_store = new WeakMap(), _JoseJwksAuthority_ttl = new WeakMap(), _JoseJwksAuthority_instances = new WeakSet(), _JoseJwksAuthority_generateAndStoreKeyPair = async function _JoseJwksAuthority_generateAndStoreKeyPair() {
|
|
189
|
+
const { publicKey, privateKey } = await generateKeyPair("RS256", {
|
|
190
|
+
modulusLength: 2048,
|
|
191
|
+
extractable: true,
|
|
192
|
+
});
|
|
193
|
+
// Convert to JWK format and add necessary properties
|
|
194
|
+
const privateJwk = await exportJWK(privateKey);
|
|
195
|
+
const publicJwk = await exportJWK(publicKey);
|
|
196
|
+
const kid = crypto.randomUUID();
|
|
197
|
+
privateJwk.kid = kid;
|
|
198
|
+
privateJwk.alg = "RS256";
|
|
199
|
+
privateJwk.use = "sig";
|
|
200
|
+
publicJwk.kid = kid;
|
|
201
|
+
publicJwk.alg = "RS256";
|
|
202
|
+
publicJwk.use = "sig";
|
|
203
|
+
await __classPrivateFieldGet(this, _JoseJwksAuthority_store, "f").storeKeyPair(kid, privateJwk, publicJwk, __classPrivateFieldGet(this, _JoseJwksAuthority_ttl, "f"));
|
|
204
|
+
return { privateJwk, publicJwk };
|
|
205
|
+
}, _JoseJwksAuthority_getPrivateKey = async function _JoseJwksAuthority_getPrivateKey() {
|
|
206
|
+
const storedPrivateJwk = await __classPrivateFieldGet(this, _JoseJwksAuthority_store, "f").getPrivateKey();
|
|
207
|
+
if (storedPrivateJwk) {
|
|
208
|
+
return storedPrivateJwk;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const { privateJwk } = await __classPrivateFieldGet(this, _JoseJwksAuthority_instances, "m", _JoseJwksAuthority_generateAndStoreKeyPair).call(this);
|
|
212
|
+
return privateJwk;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { JwksKeyStore, JwksRotationTimestampStore } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory implementation of {@link JwksKeyStore} and {@link JwksRotationTimestampStore}.
|
|
4
|
+
*
|
|
5
|
+
* Suitable for single-process deployments and testing. State is not shared
|
|
6
|
+
* across processes and is lost on restart.
|
|
7
|
+
*
|
|
8
|
+
* Use {@link createInMemoryKeyStore} as a convenience factory.
|
|
9
|
+
*/
|
|
10
|
+
export declare class InMemoryKeyStore implements JwksKeyStore, JwksRotationTimestampStore {
|
|
11
|
+
private privateKey?;
|
|
12
|
+
private publicKeys;
|
|
13
|
+
private lastRotation;
|
|
14
|
+
/**
|
|
15
|
+
* Stores the current signing key pair. The private key replaces the existing one;
|
|
16
|
+
* the public key is appended to the list with a computed expiry based on `ttl`.
|
|
17
|
+
* @param _kid - The key ID (unused by this implementation).
|
|
18
|
+
* @param privateKey - The private key object to store as the active signing key.
|
|
19
|
+
* @param publicKey - The public key object to expose in the JWKS.
|
|
20
|
+
* @param ttl - Time-to-live in seconds for the public key.
|
|
21
|
+
*/
|
|
22
|
+
storeKeyPair(_kid: string, privateKey: object, publicKey: object, ttl: number): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves the current private signing key.
|
|
25
|
+
* @returns The private key object, or `undefined` if no key has been stored yet.
|
|
26
|
+
*/
|
|
27
|
+
getPrivateKey(): Promise<object | undefined>;
|
|
28
|
+
/**
|
|
29
|
+
* Retrieves all public keys that have not yet expired.
|
|
30
|
+
* Expired keys are automatically pruned from the in-memory list on each call.
|
|
31
|
+
* @returns An array of non-expired public key objects.
|
|
32
|
+
*/
|
|
33
|
+
getPublicKeys(): Promise<object[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves the Unix timestamp (in milliseconds) of the last key rotation.
|
|
36
|
+
* @returns The timestamp of the last rotation, or `0` if no rotation has occurred.
|
|
37
|
+
*/
|
|
38
|
+
getLastRotationTimestamp(): Promise<number>;
|
|
39
|
+
/**
|
|
40
|
+
* Stores the Unix timestamp (in milliseconds) of the most recent key rotation.
|
|
41
|
+
* @param msDate - The timestamp to persist.
|
|
42
|
+
*/
|
|
43
|
+
setLastRotationTimestamp(msDate: number): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Creates a new {@link InMemoryKeyStore} instance.
|
|
47
|
+
*
|
|
48
|
+
* Convenience factory for use with {@link JoseJwksAuthority} and {@link JwksRotator}.
|
|
49
|
+
*
|
|
50
|
+
* @returns A fresh in-memory key store with no keys stored.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createInMemoryKeyStore(): InMemoryKeyStore;
|
|
53
|
+
//# sourceMappingURL=jwks_key_store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks_key_store.d.ts","sourceRoot":"","sources":["../src/jwks_key_store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAE3E;;;;;;;GAOG;AACH,qBAAa,gBAAiB,YAAW,YAAY,EAAE,0BAA0B;IAC/E,OAAO,CAAC,UAAU,CAAC,CAAS;IAC5B,OAAO,CAAC,UAAU,CAAsC;IACxD,OAAO,CAAC,YAAY,CAAa;IAEjC;;;;;;;OAOG;IACG,YAAY,CAChB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC;IAOhB;;;OAGG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAIlD;;;;OAIG;IACG,aAAa,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAMxC;;;OAGG;IACG,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC;IAIjD;;;OAGG;IACG,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAI9D;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAEzD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory implementation of {@link JwksKeyStore} and {@link JwksRotationTimestampStore}.
|
|
3
|
+
*
|
|
4
|
+
* Suitable for single-process deployments and testing. State is not shared
|
|
5
|
+
* across processes and is lost on restart.
|
|
6
|
+
*
|
|
7
|
+
* Use {@link createInMemoryKeyStore} as a convenience factory.
|
|
8
|
+
*/
|
|
9
|
+
export class InMemoryKeyStore {
|
|
10
|
+
constructor() {
|
|
11
|
+
Object.defineProperty(this, "privateKey", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
Object.defineProperty(this, "publicKeys", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: []
|
|
22
|
+
});
|
|
23
|
+
Object.defineProperty(this, "lastRotation", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: 0
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Stores the current signing key pair. The private key replaces the existing one;
|
|
32
|
+
* the public key is appended to the list with a computed expiry based on `ttl`.
|
|
33
|
+
* @param _kid - The key ID (unused by this implementation).
|
|
34
|
+
* @param privateKey - The private key object to store as the active signing key.
|
|
35
|
+
* @param publicKey - The public key object to expose in the JWKS.
|
|
36
|
+
* @param ttl - Time-to-live in seconds for the public key.
|
|
37
|
+
*/
|
|
38
|
+
async storeKeyPair(_kid, privateKey, publicKey, ttl) {
|
|
39
|
+
this.privateKey = privateKey;
|
|
40
|
+
const exp = Date.now() + ttl * 1000;
|
|
41
|
+
this.publicKeys.push({ key: publicKey, exp });
|
|
42
|
+
return await Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Retrieves the current private signing key.
|
|
46
|
+
* @returns The private key object, or `undefined` if no key has been stored yet.
|
|
47
|
+
*/
|
|
48
|
+
async getPrivateKey() {
|
|
49
|
+
return await Promise.resolve(this.privateKey);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Retrieves all public keys that have not yet expired.
|
|
53
|
+
* Expired keys are automatically pruned from the in-memory list on each call.
|
|
54
|
+
* @returns An array of non-expired public key objects.
|
|
55
|
+
*/
|
|
56
|
+
async getPublicKeys() {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
this.publicKeys = this.publicKeys.filter((k) => k.exp > now);
|
|
59
|
+
return await Promise.resolve(this.publicKeys.map((k) => k.key));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves the Unix timestamp (in milliseconds) of the last key rotation.
|
|
63
|
+
* @returns The timestamp of the last rotation, or `0` if no rotation has occurred.
|
|
64
|
+
*/
|
|
65
|
+
async getLastRotationTimestamp() {
|
|
66
|
+
return await Promise.resolve(this.lastRotation);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Stores the Unix timestamp (in milliseconds) of the most recent key rotation.
|
|
70
|
+
* @param msDate - The timestamp to persist.
|
|
71
|
+
*/
|
|
72
|
+
async setLastRotationTimestamp(msDate) {
|
|
73
|
+
this.lastRotation = msDate;
|
|
74
|
+
return await Promise.resolve();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Creates a new {@link InMemoryKeyStore} instance.
|
|
79
|
+
*
|
|
80
|
+
* Convenience factory for use with {@link JoseJwksAuthority} and {@link JwksRotator}.
|
|
81
|
+
*
|
|
82
|
+
* @returns A fresh in-memory key store with no keys stored.
|
|
83
|
+
*/
|
|
84
|
+
export function createInMemoryKeyStore() {
|
|
85
|
+
return new InMemoryKeyStore();
|
|
86
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { JwksRotationTimestampStore, KeyGenerator } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for {@link JwksRotator}.
|
|
4
|
+
*/
|
|
5
|
+
export interface JwksRotatorOptions {
|
|
6
|
+
/** The key generator used to produce new signing key pairs during rotation. */
|
|
7
|
+
keyGenerator: KeyGenerator;
|
|
8
|
+
/** The store used to read and persist the last rotation timestamp. */
|
|
9
|
+
rotatorKeyStore: JwksRotationTimestampStore;
|
|
10
|
+
/** How often (in milliseconds) new keys should be generated. For example, `7.884e9` for 91 days. */
|
|
11
|
+
rotationIntervalMs: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Manages automatic JWKS key rotation based on a configurable interval.
|
|
15
|
+
*
|
|
16
|
+
* Call {@link JwksRotator.checkAndRotateKeys} at service startup and/or on a
|
|
17
|
+
* recurring schedule (e.g. every hour) to ensure that signing keys are rotated
|
|
18
|
+
* before they expire. During rotation the previous public key remains available
|
|
19
|
+
* in the JWKS until its TTL expires, so in-flight tokens continue to verify correctly.
|
|
20
|
+
*/
|
|
21
|
+
export declare class JwksRotator {
|
|
22
|
+
private readonly keyGenerator;
|
|
23
|
+
private readonly rotatorKeyStore;
|
|
24
|
+
private readonly rotationIntervalMs;
|
|
25
|
+
/**
|
|
26
|
+
* @param options - Rotation configuration: key generator, timestamp store, and interval.
|
|
27
|
+
*/
|
|
28
|
+
constructor({ keyGenerator, rotationIntervalMs, rotatorKeyStore }: JwksRotatorOptions);
|
|
29
|
+
/**
|
|
30
|
+
* Checks if rotation is due, and performs rotation if necessary.
|
|
31
|
+
* Should be called at service startup or on a schedule (e.g. every hour).
|
|
32
|
+
*/
|
|
33
|
+
checkAndRotateKeys(): Promise<void>;
|
|
34
|
+
private rotateKeys;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=jwks_rotator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwks_rotator.d.ts","sourceRoot":"","sources":["../src/jwks_rotator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,0BAA0B,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAEtE;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,+EAA+E;IAC/E,YAAY,EAAE,YAAY,CAAC;IAC3B,sEAAsE;IACtE,eAAe,EAAE,0BAA0B,CAAC;IAC5C,oGAAoG;IACpG,kBAAkB,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAC5C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA6B;IAC7D,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAS;IAE5C;;OAEG;gBACS,EAAE,YAAY,EAAE,kBAAkB,EAAE,eAAe,EAAE,EAAE,kBAAkB;IAMrF;;;OAGG;IACU,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;YAYlC,UAAU;CAGzB"}
|