unjwt 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -361
- package/dist/_chunks/_internal.mjs +5 -5
- package/dist/_chunks/index.d.mts +175 -55
- package/dist/_chunks/jwe.d.mts +10 -8
- package/dist/_chunks/jwe.mjs +5 -5
- package/dist/_chunks/jwk.mjs +24 -9
- package/dist/_chunks/jws.d.mts +6 -6
- package/dist/_chunks/jws.mjs +5 -5
- package/dist/_chunks/libs/unsecure.d.mts +7 -7
- package/dist/_chunks/libs/unsecure.mjs +10 -5
- package/dist/_chunks/utils.mjs +2 -2
- package/dist/adapters/elysia/index.d.mts +244 -0
- package/dist/adapters/elysia/index.mjs +608 -0
- package/dist/adapters/h3v1/index.d.mts +128 -50
- package/dist/adapters/h3v1/index.mjs +18 -15
- package/dist/adapters/h3v2/index.d.mts +148 -28
- package/dist/adapters/h3v2/index.mjs +19 -16
- package/dist/core/jwe.d.mts +3 -3
- package/dist/core/jwe.mjs +2 -2
- package/dist/core/jwk.d.mts +14 -14
- package/dist/core/jwk.mjs +2 -2
- package/dist/core/jws.d.mts +3 -3
- package/dist/core/jws.mjs +2 -2
- package/dist/core/utils/index.d.mts +42 -10
- package/dist/core/utils/index.mjs +2 -2
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +4 -4
- package/package.json +26 -14
- package/dist/THIRD-PARTY-LICENSES.md +0 -63
- package/dist/_chunks/libs/rou3.mjs +0 -5
package/README.md
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
[](https://npmx.dev/package/unjwt)
|
|
5
5
|
[](https://npmx.dev/package/unjwt)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Low-level JWT toolkit built on the Web Crypto API. Sign, verify, encrypt, decrypt, and manage keys — JWS, JWE, and JWK in a single zero-dependency package.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using Direct (`dir`), AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
|
|
11
|
-
- **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
|
|
12
|
-
- **Framework adapters** for cookie-based JWT sessions:
|
|
13
|
-
- [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
|
|
14
|
-
- [H3 v2](./skills/unjwt/references/adapters-h3.md) (Nuxt v5, Nitro v3)
|
|
9
|
+
> **📖 Documentation — [unjwt.s94.dev](https://unjwt.s94.dev)**
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — HMAC, RSA, RSA-PSS, ECDSA, EdDSA. Compact + General/Flattened JSON Serialization (multi-signature).
|
|
14
|
+
- **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — `dir`, AES-KW, AES-GCM-KW, RSA-OAEP, PBES2, ECDH-ES. AES-GCM and AES-CBC-HMAC-SHA2 content encryption. Compact + General/Flattened JSON Serialization (multi-recipient).
|
|
15
|
+
- **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, unwrap. `CryptoKey` ↔ JWK ↔ PEM conversion. ECDH-ES shared-secret derivation. JWKS fetching, lookup, and rotation.
|
|
16
|
+
- **Framework adapters** — cookie-based JWT sessions for [H3 v1](https://unjwt.s94.dev/adapters) (Nuxt v4 / Nitro v2) and [H3 v2](https://unjwt.s94.dev/adapters) (Nuxt v5 / Nitro v3).
|
|
17
|
+
- **Runtime-agnostic** — Node 22+, Deno, Bun, Cloudflare Workers, browsers. Anywhere Web Crypto runs.
|
|
18
|
+
- **Zero runtime dependencies** for core; optional peer deps for adapters (`h3`, `cookie-es`, `rou3`).
|
|
17
19
|
|
|
18
20
|
## Install
|
|
19
21
|
|
|
@@ -22,376 +24,39 @@ Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cook
|
|
|
22
24
|
npx nypm install unjwt
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
Agent skill for Claude Code (and compatible harnesses):
|
|
26
28
|
|
|
27
29
|
```sh
|
|
28
30
|
npx skills add sandros94/unjwt
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
##
|
|
32
|
-
|
|
33
|
-
### JWS — Sign & Verify
|
|
34
|
-
|
|
35
|
-
> Full reference: [skills/unjwt/references/jws.md](./skills/unjwt/references/jws.md)
|
|
33
|
+
## Quickstart
|
|
36
34
|
|
|
37
35
|
```ts
|
|
36
|
+
// Sign and verify a JWT (JWS)
|
|
38
37
|
import { sign, verify } from "unjwt/jws";
|
|
39
38
|
import { generateJWK } from "unjwt/jwk";
|
|
40
|
-
// or import both from the root: import { sign, verify, generateJWK } from "unjwt";
|
|
41
39
|
|
|
42
|
-
// Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
|
|
43
40
|
const key = await generateJWK("HS256");
|
|
41
|
+
const token = await sign({ sub: "user_1" }, key, { expiresIn: "1h" });
|
|
42
|
+
const { payload } = await verify(token, key);
|
|
43
|
+
// { sub: "user_1", iat: ..., exp: ... }
|
|
44
44
|
|
|
45
|
-
//
|
|
46
|
-
const token = await sign(
|
|
47
|
-
{ sub: "user123", role: "admin" },
|
|
48
|
-
key,
|
|
49
|
-
{ expiresIn: "1h" }, // supports: number (seconds), "30s", "10m", "2h", "7D", "1Y"
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
// Verify and extract payload
|
|
53
|
-
const { payload, protectedHeader } = await verify(token, key);
|
|
54
|
-
console.log(payload); // { sub: "user123", role: "admin", iat: ..., exp: ... }
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
**Asymmetric keys (RS256, ES256, PS256, Ed25519, etc.):**
|
|
58
|
-
|
|
59
|
-
```ts
|
|
60
|
-
const keys = await generateJWK("RS256");
|
|
61
|
-
const token = await sign({ sub: "user123" }, keys.privateKey);
|
|
62
|
-
const { payload } = await verify(token, keys.publicKey);
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**Key lookup function for dynamic key resolution:**
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
const { payload } = await verify(
|
|
69
|
-
token,
|
|
70
|
-
async (header) => {
|
|
71
|
-
// Resolve key by kid, alg, or any header field
|
|
72
|
-
return await fetchPublicKeyByKid(header.kid);
|
|
73
|
-
},
|
|
74
|
-
{ algorithms: ["RS256"] },
|
|
75
|
-
);
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
**JWKSet for key rotation and JWKS endpoint consumption:**
|
|
79
|
-
|
|
80
|
-
When a `JWKSet` is passed (directly or returned from a lookup function), `verify` tries each matching key in order until one succeeds. If the token carries a `kid`, only keys with that exact `kid` are tried — no retry. If `kid` is absent, all keys in the set whose `alg` is compatible become candidates. This makes transparent key rotation possible without any retry logic in userland.
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
// Fetch all public keys from a JWKS endpoint — library picks the right one
|
|
84
|
-
const jwks = await fetch("https://auth.example.com/.well-known/jwks.json").then((r) => r.json());
|
|
85
|
-
const { payload } = await verify(token, jwks);
|
|
86
|
-
|
|
87
|
-
// Rotation window: old tokens (no kid) are tried against all keys automatically
|
|
88
|
-
const rotatingSet = { keys: [newKey, legacyKey] };
|
|
89
|
-
const { payload: p } = await verify(oldToken, rotatingSet);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### JWE — Encrypt & Decrypt
|
|
93
|
-
|
|
94
|
-
> Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
import { encrypt, decrypt } from "unjwt/jwe";
|
|
98
|
-
// or: import { encrypt, decrypt } from "unjwt";
|
|
99
|
-
|
|
100
|
-
// Password-based encryption (simplest — uses PBES2)
|
|
101
|
-
const token = await encrypt({ secret: "sensitive data" }, "my-password");
|
|
102
|
-
const { payload } = await decrypt(token, "my-password");
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
**With explicit key management:**
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
import { generateJWK } from "unjwt/jwk";
|
|
109
|
-
|
|
110
|
-
// AES Key Wrap
|
|
111
|
-
const aesKey = await generateJWK("A256KW");
|
|
112
|
-
const token = await encrypt({ data: "secret" }, aesKey);
|
|
113
|
-
|
|
114
|
-
// RSA-OAEP
|
|
115
|
-
const rsaKeys = await generateJWK("RSA-OAEP-256");
|
|
116
|
-
const token2 = await encrypt({ data: "secret" }, rsaKeys.publicKey);
|
|
117
|
-
const { payload } = await decrypt(token2, rsaKeys.privateKey);
|
|
118
|
-
|
|
119
|
-
// ECDH-ES
|
|
120
|
-
const ecKeys = await generateJWK("ECDH-ES+A256KW");
|
|
121
|
-
const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### ECDH-ES — End-to-End Encrypted Messaging
|
|
125
|
-
|
|
126
|
-
In end-to-end encryption (E2EE), each participant holds a **key pair**: the **private key** never leaves the owner's system; the **public key** is distributed freely. A sender encrypts a message using the recipient's public key, producing a token that only the recipient — holding the matching private key — can decrypt.
|
|
127
|
-
|
|
128
|
-
#### One-time key setup
|
|
129
|
-
|
|
130
|
-
Each participant generates a key pair once and persists it. The public key is shared out-of-band — published to a server endpoint, written to a config file, exchanged at registration, etc.:
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
import { generateJWK } from "unjwt/jwk";
|
|
134
|
-
|
|
135
|
-
const myKeys = await generateJWK("ECDH-ES+A256KW", { kid: "alice-2025" });
|
|
136
|
-
// → { privateKey: JWK_EC_Private, publicKey: JWK_EC_Public }
|
|
137
|
-
|
|
138
|
-
// Keep myKeys.privateKey in secure storage — never expose it
|
|
139
|
-
// Share myKeys.publicKey freely (e.g., publish at /.well-known/jwks.json)
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
#### Sending to one recipient
|
|
143
|
-
|
|
144
|
-
For two-party communication, `encrypt` and `decrypt` handle everything. Pass the recipient's public key — a one-time ephemeral key pair is generated internally per message:
|
|
145
|
-
|
|
146
|
-
```ts
|
|
147
|
-
import { encrypt, decrypt } from "unjwt/jwe";
|
|
148
|
-
|
|
149
|
-
// Sender (Alice) ————————————————————————————————————
|
|
150
|
-
// Fetch Bob's public key (from his public endpoint, shared config, etc.)
|
|
151
|
-
const bobPublicKey = /* ... */;
|
|
152
|
-
|
|
153
|
-
const token = await encrypt({ message: "Hello Bob!" }, bobPublicKey);
|
|
154
|
-
// Send `token` to Bob over any channel — only Bob can decrypt it
|
|
155
|
-
|
|
156
|
-
// Recipient (Bob) ————————————————————————————————————
|
|
157
|
-
const { payload } = await decrypt(token, bobPrivateKey);
|
|
158
|
-
console.log(payload.message); // "Hello Bob!"
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Each token carries a fresh ephemeral public key (`epk`) in its header. Bob uses that `epk` together with his private key to re-derive the same shared secret Alice used, then unwraps the Content Encryption Key. At no point does Alice have Bob's private key, and Bob cannot forge a message appearing to come from Alice.
|
|
162
|
-
|
|
163
|
-
#### Sending to multiple recipients
|
|
164
|
-
|
|
165
|
-
**Simple — one token per recipient.** Encrypt the payload independently for each party. Each token is decryptable only by its intended recipient:
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
const recipients = [
|
|
169
|
-
{ name: "bob", publicKey: bobPublicKey },
|
|
170
|
-
{ name: "charlie", publicKey: charliePublicKey },
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
const tokens = await Promise.all(
|
|
174
|
-
recipients.map(({ publicKey }) => encrypt({ message: "Hello everyone!" }, publicKey)),
|
|
175
|
-
);
|
|
176
|
-
// Deliver tokens[0] to Bob, tokens[1] to Charlie
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
This is the right choice for most use cases. The only trade-off is proportional size: N recipients produce N tokens.
|
|
180
|
-
|
|
181
|
-
**Shared ciphertext — one payload, N wrapped keys.** When the payload is large or bandwidth matters, encrypt the payload once with a random Content Encryption Key (`dir`), then wrap that key separately per recipient. Everyone receives the same ciphertext, each with their own individually wrapped key:
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
import { wrapKey, unwrapKey } from "unjwt/jwk";
|
|
185
|
-
import { randomBytes } from "unjwt/utils";
|
|
45
|
+
// Encrypt and decrypt (JWE)
|
|
186
46
|
import { encrypt, decrypt } from "unjwt/jwe";
|
|
187
47
|
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
// 1. Generate a random CEK once for this message (32 bytes = 256-bit for A256GCM)
|
|
191
|
-
const cek = randomBytes(32);
|
|
192
|
-
|
|
193
|
-
// 2. Encrypt the payload ONCE using the CEK directly (alg: "dir")
|
|
194
|
-
const ciphertext = await encrypt({ message: "Hello everyone!" }, cek, { alg: "dir", enc });
|
|
195
|
-
|
|
196
|
-
// 3. Wrap the CEK individually for each recipient
|
|
197
|
-
const wrappedKeys = await Promise.all(
|
|
198
|
-
recipients.map(async ({ name, publicKey }) => {
|
|
199
|
-
const { encryptedKey, epk } = await wrapKey("ECDH-ES+A256KW", cek, publicKey);
|
|
200
|
-
return { name, encryptedKey, epk };
|
|
201
|
-
}),
|
|
202
|
-
);
|
|
203
|
-
// Deliver `ciphertext` (same for all) + each recipient's own { encryptedKey, epk }
|
|
204
|
-
|
|
205
|
-
// Recipient side ——————————————————————————————————————————————
|
|
206
|
-
const mine = wrappedKeys.find((w) => w.name === myName)!;
|
|
207
|
-
const rawCek = await unwrapKey("ECDH-ES+A256KW", mine.encryptedKey, myPrivateKey, {
|
|
208
|
-
format: "raw",
|
|
209
|
-
epk: mine.epk,
|
|
210
|
-
enc,
|
|
211
|
-
});
|
|
212
|
-
const { payload } = await decrypt(ciphertext, rawCek);
|
|
213
|
-
console.log(payload.message); // "Hello everyone!"
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
> This pattern is the manual foundation of [JWE JSON Serialization](https://www.rfc-editor.org/rfc/rfc7516#section-3.3) (RFC 7516 §3.3), which packages the ciphertext and all wrapped keys into a single `recipients[]` JSON structure. Full JWE JSON Serialization support is planned for a future release; `wrapKey` and `unwrapKey` provide the building blocks for those who need to construct or consume the JSON form today.
|
|
217
|
-
|
|
218
|
-
#### `deriveSharedSecret` — raw key material
|
|
219
|
-
|
|
220
|
-
`wrapKey` and `unwrapKey` already handle the full ECDH Concat KDF + AES-KW cycle internally. `deriveSharedSecret` exposes the KDF step alone, returning the raw derived bytes before any key-wrapping step:
|
|
221
|
-
|
|
222
|
-
```ts
|
|
223
|
-
import { deriveSharedSecret } from "unjwt/jwk";
|
|
224
|
-
|
|
225
|
-
// Both sides independently derive the exact same bytes
|
|
226
|
-
const aliceView = await deriveSharedSecret(
|
|
227
|
-
bobPublicKey,
|
|
228
|
-
aliceEphemeralPrivateKey,
|
|
229
|
-
"ECDH-ES+A256KW",
|
|
230
|
-
);
|
|
231
|
-
const bobView = await deriveSharedSecret(aliceEphemeralPublicKey, bobPrivateKey, "ECDH-ES+A256KW");
|
|
232
|
-
// aliceView and bobView are identical Uint8Arrays
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
Use `deriveSharedSecret` when you need the shared secret itself rather than a wrapped key: custom hybrid protocols that use the derived bytes directly, non-standard wrapping schemes, or verifying the key agreement step in isolation.
|
|
236
|
-
|
|
237
|
-
### JWK — Key Management
|
|
238
|
-
|
|
239
|
-
> Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
|
|
240
|
-
|
|
241
|
-
```ts
|
|
242
|
-
import {
|
|
243
|
-
generateKey,
|
|
244
|
-
generateJWK,
|
|
245
|
-
importKey,
|
|
246
|
-
exportKey,
|
|
247
|
-
importFromPEM,
|
|
248
|
-
exportToPEM,
|
|
249
|
-
wrapKey,
|
|
250
|
-
unwrapKey,
|
|
251
|
-
deriveKeyFromPassword,
|
|
252
|
-
getJWKsFromSet,
|
|
253
|
-
deriveSharedSecret,
|
|
254
|
-
configureJWKCache,
|
|
255
|
-
} from "unjwt/jwk";
|
|
256
|
-
// All of the above are also importable from "unjwt" directly.
|
|
257
|
-
|
|
258
|
-
// Generate keys as CryptoKey or JWK
|
|
259
|
-
const hmacKey = await generateKey("HS256"); // CryptoKey
|
|
260
|
-
const rsaPair = await generateKey("RS256"); // CryptoKeyPair
|
|
261
|
-
const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
|
|
262
|
-
|
|
263
|
-
// PEM conversion
|
|
264
|
-
const jwk = await importFromPEM(pemString, "spki", "RS256", { jwkParams: { kid: "rsa-1" } });
|
|
265
|
-
const pem = await exportToPEM(jwk, "spki");
|
|
266
|
-
|
|
267
|
-
// Key wrapping
|
|
268
|
-
const cek = crypto.getRandomValues(new Uint8Array(32));
|
|
269
|
-
const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
|
|
270
|
-
const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { format: "raw" });
|
|
271
|
-
|
|
272
|
-
// ECDH-ES shared secret (e.g. for multi-recipient JWE)
|
|
273
|
-
const secret = await deriveSharedSecret(
|
|
274
|
-
recipientPublicKey,
|
|
275
|
-
senderEphemeralPrivate,
|
|
276
|
-
"ECDH-ES+A256KW",
|
|
277
|
-
);
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
### Utility Functions
|
|
281
|
-
|
|
282
|
-
> Full reference: [skills/unjwt/references/utils.md](./skills/unjwt/references/utils.md)
|
|
283
|
-
|
|
284
|
-
```ts
|
|
285
|
-
import {
|
|
286
|
-
base64UrlEncode,
|
|
287
|
-
base64UrlDecode,
|
|
288
|
-
randomBytes,
|
|
289
|
-
isJWK,
|
|
290
|
-
isJWKSet,
|
|
291
|
-
isSymmetricJWK,
|
|
292
|
-
isPrivateJWK,
|
|
293
|
-
isPublicJWK,
|
|
294
|
-
isCryptoKey,
|
|
295
|
-
textEncoder,
|
|
296
|
-
textDecoder,
|
|
297
|
-
} from "unjwt/utils";
|
|
298
|
-
// Most of the above are also importable from "unjwt" directly.
|
|
299
|
-
|
|
300
|
-
const bytes = randomBytes(32);
|
|
301
|
-
const encoded = base64UrlEncode(bytes);
|
|
302
|
-
const decoded = base64UrlDecode(encoded, false); // Uint8Array
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### H3 Session Adapters
|
|
306
|
-
|
|
307
|
-
> Full reference: [skills/unjwt/references/adapters-h3.md](./skills/unjwt/references/adapters-h3.md)
|
|
308
|
-
|
|
309
|
-
Cookie-based JWT session management for H3 applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies.
|
|
310
|
-
|
|
311
|
-
> **Note:** Sessions are lazy — `session.id` is `undefined` until `session.update()` is called. This is intentional for OAuth/spec-compliant flows where sessions should only be created upon valid operations.
|
|
312
|
-
|
|
313
|
-
#### JWE Session (encrypted, recommended for sensitive data)
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
import { useJWESession, generateJWK } from "unjwt/adapters/h3v2";
|
|
317
|
-
|
|
318
|
-
app.get("/profile", async (event) => {
|
|
319
|
-
const session = await useJWESession(event, {
|
|
320
|
-
key: process.env.SESSION_SECRET!, // password string, symmetric JWK, or asymmetric keypair
|
|
321
|
-
maxAge: "7D",
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
if (!session.id) {
|
|
325
|
-
// No active session — create one
|
|
326
|
-
await session.update({ userId: "123", email: "user@example.com" });
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return { user: session.data };
|
|
330
|
-
});
|
|
48
|
+
const jwe = await encrypt({ secret: "data" }, "my-password");
|
|
49
|
+
const { payload: p } = await decrypt(jwe, "my-password");
|
|
331
50
|
```
|
|
332
51
|
|
|
333
|
-
|
|
52
|
+
Full walkthroughs, recipes, and the complete API reference live at **[unjwt.s94.dev](https://unjwt.s94.dev)**:
|
|
334
53
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const session = await useJWSSession(event, {
|
|
342
|
-
key: keys,
|
|
343
|
-
maxAge: "24h",
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
return { theme: session.data.theme };
|
|
347
|
-
});
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
#### Session features
|
|
351
|
-
|
|
352
|
-
- **Cookie chunking** — large tokens are automatically split across multiple cookies
|
|
353
|
-
- **Header-based tokens** — read from `Authorization: Bearer <token>` or custom headers via `sessionHeader`
|
|
354
|
-
- **Lifecycle hooks** — `onRead`, `onUpdate`, `onClear`, `onExpire`, `onError` for logging, refresh logic, etc.
|
|
355
|
-
- **Key lookup hooks** — `onUnsealKeyLookup` (JWE) / `onVerifyKeyLookup` (JWS) for key rotation
|
|
356
|
-
- **TypeScript generics** — strongly-typed session data via `useJWESession<MyData>(event, config)`
|
|
357
|
-
|
|
358
|
-
#### Refresh token pattern
|
|
359
|
-
|
|
360
|
-
```ts
|
|
361
|
-
import {
|
|
362
|
-
type SessionConfigJWE,
|
|
363
|
-
type SessionConfigJWS,
|
|
364
|
-
useJWESession,
|
|
365
|
-
useJWSSession,
|
|
366
|
-
getJWESession,
|
|
367
|
-
updateJWSSession,
|
|
368
|
-
generateJWK,
|
|
369
|
-
} from "unjwt/adapters/h3v2";
|
|
370
|
-
|
|
371
|
-
const atKeys = await generateJWK("RS256");
|
|
372
|
-
|
|
373
|
-
const refreshConfig = {
|
|
374
|
-
key: process.env.REFRESH_SECRET!,
|
|
375
|
-
name: "refresh_token",
|
|
376
|
-
} satisfies SessionConfigJWE;
|
|
377
|
-
|
|
378
|
-
const accessConfig = {
|
|
379
|
-
key: atKeys,
|
|
380
|
-
name: "access_token",
|
|
381
|
-
maxAge: "15m",
|
|
382
|
-
hooks: {
|
|
383
|
-
async onExpire({ event, config }) {
|
|
384
|
-
const refresh = await getJWESession(event, refreshConfig);
|
|
385
|
-
if (refresh.data.sub) {
|
|
386
|
-
await updateJWSSession(event, config, {
|
|
387
|
-
sub: refresh.data.sub,
|
|
388
|
-
scope: refresh.data.scope,
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
} satisfies SessionConfigJWS;
|
|
394
|
-
```
|
|
54
|
+
- [Getting started →](https://unjwt.s94.dev/getting-started)
|
|
55
|
+
- [JWS — signing and verifying →](https://unjwt.s94.dev/jwt/jws)
|
|
56
|
+
- [JWE — encrypting and decrypting →](https://unjwt.s94.dev/jwt/jwe)
|
|
57
|
+
- [JWK — key management →](https://unjwt.s94.dev/jwk)
|
|
58
|
+
- [H3 session adapters →](https://unjwt.s94.dev/adapters)
|
|
59
|
+
- [Examples →](https://unjwt.s94.dev/examples) — authentication, JWKS endpoints, refresh tokens, end-to-end encryption, signed receipts.
|
|
395
60
|
|
|
396
61
|
## Development
|
|
397
62
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { JWTError, decodePayloadFromB64UrlSegment, inferJWEAllowedAlgorithms, inferJWSAllowedAlgorithms, isCryptoKey, isCryptoKeyPair, isJWK, validateJwtClaims } from "./utils.mjs";
|
|
2
|
+
import { base64UrlDecode, safeJsonParse, textEncoder } from "./libs/unsecure.mjs";
|
|
3
|
+
import { assertOctKeyLength } from "./jwk.mjs";
|
|
3
4
|
function buildJWKSetFilter(header) {
|
|
4
5
|
const kid = header?.kid;
|
|
5
6
|
const alg = header?.alg;
|
|
@@ -35,8 +36,7 @@ function parseEphemeralKey(ephemeralKey) {
|
|
|
35
36
|
}
|
|
36
37
|
async function resolveSigningKey(alg, key, usage) {
|
|
37
38
|
if (key instanceof Uint8Array) {
|
|
38
|
-
|
|
39
|
-
if (key.length < minBytes) throw new JWTError(`${alg} requires a key of at least ${minBytes} bytes`, "ERR_JWK_INVALID");
|
|
39
|
+
assertOctKeyLength(key, alg);
|
|
40
40
|
return crypto.subtle.importKey("raw", key, {
|
|
41
41
|
name: "HMAC",
|
|
42
42
|
hash: `SHA-${alg.slice(-3)}`
|
|
@@ -85,4 +85,4 @@ function decodeJWSPayload(payloadSegment, useB64, forceUint8Array) {
|
|
|
85
85
|
if (useB64) return decodePayloadFromB64UrlSegment(payloadSegment, forceUint8Array);
|
|
86
86
|
return forceUint8Array ? textEncoder.encode(payloadSegment) : payloadSegment;
|
|
87
87
|
}
|
|
88
|
-
export {
|
|
88
|
+
export { JWE_ALG_CTX, JWS_ALG_CTX, buildJWKSetFilter, checkAlgAllowed, decodeJWSPayload, decodeProtectedHeader, parseEphemeralKey, resolveSigningKey, validateJwtClaimsIfJsonPayload };
|