unjwt 0.5.38 → 0.5.39
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 +142 -666
- package/dist/_chunks/chunk.mjs +13 -0
- package/dist/_chunks/h3v1.mjs +598 -0
- package/dist/_chunks/index.d.mts +250 -0
- package/dist/_chunks/index2.d.mts +108 -0
- package/dist/_chunks/jwe.d.mts +43 -0
- package/dist/_chunks/jwe.mjs +132 -0
- package/dist/_chunks/jwk.d.mts +575 -0
- package/dist/_chunks/jwk.mjs +1592 -0
- package/dist/_chunks/jws.d.mts +46 -0
- package/dist/_chunks/jws.mjs +2069 -0
- package/dist/_chunks/utils.mjs +374 -0
- package/dist/adapters/h3.d.mts +3 -267
- package/dist/adapters/h3.mjs +3 -704
- package/dist/adapters/h3v1.d.mts +3 -8
- package/dist/adapters/h3v1.mjs +3 -7
- package/dist/adapters/h3v2.d.mts +145 -146
- package/dist/adapters/h3v2.mjs +505 -594
- package/dist/index.d.mts +6 -8
- package/dist/index.mjs +5 -4
- package/dist/jwe.d.mts +3 -4
- package/dist/jwe.mjs +4 -3
- package/dist/jwk.d.mts +3 -2
- package/dist/jwk.mjs +3 -2
- package/dist/jws.d.mts +3 -3
- package/dist/jws.mjs +4 -3
- package/dist/utils.d.mts +3 -3
- package/dist/utils.mjs +2 -1
- package/package.json +33 -31
- package/dist/shared/unjwt.4JQCoID_.d.mts +0 -161
- package/dist/shared/unjwt.B-v62plj.mjs +0 -249
- package/dist/shared/unjwt.BCi3FEWr.d.mts +0 -328
- package/dist/shared/unjwt.BYDTEWa-.d.mts +0 -269
- package/dist/shared/unjwt.Be8NxwFK.mjs +0 -438
- package/dist/shared/unjwt.BqdNUKDr.d.mts +0 -64
- package/dist/shared/unjwt.CMNtw8ek.d.mts +0 -155
- package/dist/shared/unjwt.Cy2MlkUe.d.mts +0 -174
- package/dist/shared/unjwt.D0_ccT7g.d.mts +0 -166
- package/dist/shared/unjwt.DdLN9vwI.mjs +0 -163
- package/dist/shared/unjwt.Dpl__ZAR.mjs +0 -2121
- package/dist/shared/unjwt.JdKqVANs.d.mts +0 -5
package/README.md
CHANGED
|
@@ -10,654 +10,211 @@
|
|
|
10
10
|
|
|
11
11
|
A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
|
|
12
12
|
|
|
13
|
-
- **JWS (
|
|
14
|
-
- **JWE (
|
|
15
|
-
- **JWK (
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
- [H3
|
|
19
|
-
- [H3 v2 (Nuxt v5, Nitro v3)](./src/adapters/h3v2/): `useJWSSession()`, `useJWESession()`
|
|
20
|
-
|
|
21
|
-
## Table of Contents
|
|
22
|
-
|
|
23
|
-
- [Usage](#usage)
|
|
24
|
-
- [JWS](#jws-json-web-signature-rfc-7515)
|
|
25
|
-
- [JWE](#jwe-json-web-encryption-rfc-7516)
|
|
26
|
-
- [JWK](#jwk-json-web-key-rfc-7517)
|
|
27
|
-
- [Utility Functions](#utility-functions)
|
|
28
|
-
- [Adapters](#adapters)
|
|
29
|
-
- [H3 v1 (Nuxt up to v4, Nitro up to v2)](#h3-v1-nuxt-v4-nitro-v2)
|
|
30
|
-
- [H3 v2 (Nuxt v5, Nitro v3)](#h3-v2-nuxt-v5-nitro-v3)
|
|
31
|
-
- [Development](#development)
|
|
32
|
-
- [Credits](#credits)
|
|
33
|
-
- [License](#license)
|
|
13
|
+
- **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — sign and verify tokens using HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithms
|
|
14
|
+
- **JWE** ([RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516)) — encrypt and decrypt data using AES Key Wrap, AES-GCM KW, RSA-OAEP, PBES2, and ECDH-ES key management with AES-GCM or AES-CBC+HMAC-SHA2 content encryption
|
|
15
|
+
- **JWK** ([RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517)) — generate, import, export, wrap, and unwrap keys (CryptoKey, JWK, PEM)
|
|
16
|
+
- **Framework adapters** for cookie-based JWT sessions:
|
|
17
|
+
- [H3 v1](./skills/unjwt/references/adapters-h3.md) (Nuxt v4, Nitro v2)
|
|
18
|
+
- [H3 v2](./skills/unjwt/references/adapters-h3.md) (Nuxt v5, Nitro v3)
|
|
34
19
|
|
|
35
|
-
|
|
20
|
+
Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cookie-es`, `rou3`).
|
|
36
21
|
|
|
37
|
-
Install
|
|
22
|
+
## Install
|
|
38
23
|
|
|
39
24
|
```sh
|
|
40
|
-
#
|
|
25
|
+
# Auto-detect package manager (npm, yarn, pnpm, deno, bun)
|
|
41
26
|
npx nypm install unjwt
|
|
42
27
|
```
|
|
43
28
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
**ESM** (Node.js, Bun, Deno)
|
|
47
|
-
|
|
48
|
-
```js
|
|
49
|
-
import { jws, jwe, jwk } from "unjwt";
|
|
50
|
-
// JWS functions
|
|
51
|
-
import { sign, verify } from "unjwt/jws";
|
|
52
|
-
// JWE functions
|
|
53
|
-
import { encrypt, decrypt } from "unjwt/jwe";
|
|
54
|
-
// JWK functions
|
|
55
|
-
import {
|
|
56
|
-
generateKey,
|
|
57
|
-
generateJWK,
|
|
58
|
-
importJWKFromPEM,
|
|
59
|
-
exportJWKToPEM,
|
|
60
|
-
deriveKeyFromPassword,
|
|
61
|
-
deriveJWKFromPassword,
|
|
62
|
-
} from "unjwt/jwk";
|
|
63
|
-
// Utility functions
|
|
64
|
-
import {
|
|
65
|
-
isJWK,
|
|
66
|
-
isJWKSet,
|
|
67
|
-
isSymmetricJWK,
|
|
68
|
-
isAsymmetricJWK,
|
|
69
|
-
isPrivateJWK,
|
|
70
|
-
isPublicJWK,
|
|
71
|
-
} from "unjwt/utils";
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**CDN** (Deno, Bun and Browsers)
|
|
75
|
-
|
|
76
|
-
```js
|
|
77
|
-
import { jws, jwe, jwk } from "https://esm.sh/unjwt";
|
|
78
|
-
// JWS functions
|
|
79
|
-
import { sign, verify } from "https://esm.sh/unjwt/jws";
|
|
80
|
-
// JWE functions
|
|
81
|
-
import { encrypt, decrypt } from "https://esm.sh/unjwt/jwe";
|
|
82
|
-
// JWK functions
|
|
83
|
-
import {
|
|
84
|
-
generateKey,
|
|
85
|
-
generateJWK,
|
|
86
|
-
importJWKFromPEM,
|
|
87
|
-
exportJWKToPEM,
|
|
88
|
-
deriveKeyFromPassword,
|
|
89
|
-
deriveJWKFromPassword,
|
|
90
|
-
} from "https://esm.sh/unjwt/jwk";
|
|
91
|
-
// Utility functions
|
|
92
|
-
import {
|
|
93
|
-
isJWK,
|
|
94
|
-
isJWKSet,
|
|
95
|
-
isSymmetricJWK,
|
|
96
|
-
isAsymmetricJWK,
|
|
97
|
-
isPrivateJWK,
|
|
98
|
-
isPublicJWK,
|
|
99
|
-
} from "https://esm.sh/unjwt/utils";
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
### JWS (JSON Web Signature, [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515))
|
|
105
|
-
|
|
106
|
-
Functions to sign and verify data according to the JWS specification.
|
|
107
|
-
|
|
108
|
-
#### `sign(payload, key, options)`
|
|
109
|
-
|
|
110
|
-
Creates a JWS token.
|
|
111
|
-
|
|
112
|
-
- `payload`: The data to sign (`string`, `Uint8Array`, or a JSON-serializable `object`).
|
|
113
|
-
- `key`: The signing key (`CryptoKey`, `JWK`, or `Uint8Array` for symmetric keys).
|
|
114
|
-
- `options`:
|
|
115
|
-
- `alg`: (Required) The JWS algorithm (e.g., `"HS256"`, `"RS256"`, `"ES256"`, `"PS256"`).
|
|
116
|
-
- `protectedHeader`: An object for additional JWS Protected Header parameters (e.g., `kid`, `typ`, `cty`, `crit`, `b64`). If `payload` is an object and `typ` is not set, it defaults to `"JWT"`. The `b64` parameter ([RFC7797 section-3](https://datatracker.ietf.org/doc/html/rfc7797#section-3)) controls payload encoding (defaults to `true`, meaning Base64URL encoded).
|
|
117
|
-
- `expiresIn`: Sets an expiration time in seconds (e.g., `3600` for 1 hour). If `iat` is missing it will be set to the current time. If `exp` is missing it will be set to `iat + expiresIn`. This is only applied if `payload` is a JWT.
|
|
118
|
-
- `currentDate`: The current date for computing `expiresIn` option. Defaults to `new Date()`.
|
|
119
|
-
|
|
120
|
-
Returns a `Promise<string>` resolving to the JWS token in Compact Serialization format.
|
|
121
|
-
|
|
122
|
-
**Example (HS256 with string secret):**
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
import { sign } from "unjwt/jws";
|
|
126
|
-
|
|
127
|
-
const payload = { message: "My important data" }; // Object payload
|
|
128
|
-
const key = await generateKey("HS256", { toJWK: true }); // Generates a JWK_oct
|
|
129
|
-
|
|
130
|
-
const token = await sign(payload, key);
|
|
131
|
-
|
|
132
|
-
console.log(token);
|
|
133
|
-
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
**Example (RS256 with CryptoKey):**
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
import { sign } from "unjwt/jws";
|
|
140
|
-
import { generateKey } from "unjwt/jwk";
|
|
141
|
-
|
|
142
|
-
const payload = { userId: 123, permissions: ["read"] };
|
|
143
|
-
const { privateKey } = await generateKey("RS256"); // Generates a CryptoKeyPair
|
|
144
|
-
|
|
145
|
-
const token = await sign(payload, privateKey, { alg: "RS256" });
|
|
146
|
-
|
|
147
|
-
console.log(token);
|
|
148
|
-
// eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
#### `verify(jws, key, options?)`
|
|
152
|
-
|
|
153
|
-
Verifies a JWS token.
|
|
154
|
-
|
|
155
|
-
- `jws`: The JWS token string.
|
|
156
|
-
- `key`: The verification key (`CryptoKey`, `JWK`, `JWKSet`, `Uint8Array`, or a `KeyLookupFunction`).
|
|
157
|
-
A `KeyLookupFunction` has the signature `(header: JWSProtectedHeader, jws: string) => Promise<CryptoKey | JWK | JWKSet | Uint8Array> | CryptoKey | JWK | JWKSet | Uint8Array`.
|
|
158
|
-
- `options` (optional):
|
|
159
|
-
- `algorithms`: An array of allowed JWS `alg` values. If not provided, the `alg` from the JWS header is used.
|
|
160
|
-
- `validateJWT`: Unless false, will parse payload as JWT and validate claims if applicable (typ includes "jwt", case insensitive). Default `undefined`.
|
|
161
|
-
- `critical`: An array of JWS header parameter names that the application understands and processes.
|
|
162
|
-
|
|
163
|
-
Returns a `Promise<JWSVerifyResult<T>>` which is an object `{ payload: T, protectedHeader: JWSProtectedHeader }`.
|
|
164
|
-
The `payload` type `T` can be `JWTClaims` (object), `string`, or `Uint8Array` depending on the JWS content and headers.
|
|
165
|
-
|
|
166
|
-
**Example (HS256):**
|
|
167
|
-
|
|
168
|
-
```ts
|
|
169
|
-
import { verify } from "unjwt/jws";
|
|
170
|
-
|
|
171
|
-
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From HS256 sign example
|
|
172
|
-
const secret = "supersecretkey";
|
|
173
|
-
|
|
174
|
-
const { payload, protectedHeader } = await verify(token, secret);
|
|
175
|
-
console.log(payload); // { message: "My important data" }
|
|
176
|
-
console.log(protectedHeader); // { alg: "HS256", typ: "JWT" }
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
**Example (RS256 with key lookup):**
|
|
180
|
-
|
|
181
|
-
```ts
|
|
182
|
-
import { verify } from "unjwt/jws";
|
|
183
|
-
import { generateKey } from "unjwt/jwk";
|
|
184
|
-
|
|
185
|
-
const token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; // From RS256 sign example
|
|
186
|
-
|
|
187
|
-
// Example: using a key lookup function
|
|
188
|
-
const { publicKey: rsaPublicKey } = await generateKey("RS256"); // For example purposes, assume publicKey is stored/fetched during lookup
|
|
189
|
-
|
|
190
|
-
const keyLookup = async (header) => {
|
|
191
|
-
if (header.alg === "RS256" /* && header.kid === 'expected-kid' */) {
|
|
192
|
-
return rsaPublicKey;
|
|
193
|
-
}
|
|
194
|
-
throw new Error("Unsupported algorithm or key not found");
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
const { payload } = await verify(token, keyLookup, { algorithms: ["RS256"] });
|
|
198
|
-
console.log(payload); // { userId: 123, permissions: ["read"] }
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
### JWE (JSON Web Encryption, [RFC 7516](https://datatracker.ietf.org/doc/html/rfc7516))
|
|
204
|
-
|
|
205
|
-
Functions to encrypt and decrypt data according to the JWE specification.
|
|
206
|
-
|
|
207
|
-
#### `encrypt(payload, key, options)`
|
|
208
|
-
|
|
209
|
-
Encrypts data to produce a JWE token.
|
|
210
|
-
|
|
211
|
-
- `payload`: The data to encrypt (`string`, `Uint8Array`, or a JSON-serializable `object`).
|
|
212
|
-
- `key`: The Key Encryption Key (KEK) or password (`CryptoKey`, `JWK`, `string`, or `Uint8Array`).
|
|
213
|
-
- `options`:
|
|
214
|
-
- `alg`: (Required) The JWE Key Management algorithm (e.g., `"A128KW"`, `"RSA-OAEP-256"`, `"PBES2-HS256+A128KW"`, `"ECDH-ES+A128KW"`), defaults depends on the key provided.
|
|
215
|
-
- `enc`: (Required) The JWE Content Encryption algorithm (e.g., `"A128GCM"`, `"A256CBC-HS512"`), defaults depends on the key provided.
|
|
216
|
-
- `protectedHeader`: An object for JWE Protected Header parameters (e.g., `kid`, `typ`, `cty`, `crit`, `apu`, `apv`, `p2s`, `p2c`). If `payload` is an object and `typ` is not set, it defaults to `"JWT"`.
|
|
217
|
-
- `cek`: (Optional) Provide your own Content Encryption Key (`CryptoKey` or `Uint8Array`).
|
|
218
|
-
- `contentEncryptionIV`: (Optional) Provide your own Initialization Vector for content encryption (`Uint8Array`).
|
|
219
|
-
- Other algorithm-specific options like `p2s`, `p2c` (for PBES2), `keyManagementIV`, `ecdhPartyUInfo`, `ecdhPartyVInfo`.
|
|
220
|
-
|
|
221
|
-
Returns a `Promise<string>` resolving to the JWE token in Compact Serialization format.
|
|
222
|
-
|
|
223
|
-
**Example (PBES2 password-based encryption):**
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
import { encrypt } from "unjwt/jwe";
|
|
227
|
-
|
|
228
|
-
const plaintext = "Secret message for password protection";
|
|
229
|
-
const password = "myVeryStrongPassword123!";
|
|
29
|
+
## Usage
|
|
230
30
|
|
|
231
|
-
|
|
232
|
-
const jweToken = await encrypt(plaintext, password);
|
|
233
|
-
console.log(jweToken);
|
|
234
|
-
// JWE token string...
|
|
235
|
-
```
|
|
31
|
+
### JWS — Sign & Verify
|
|
236
32
|
|
|
237
|
-
|
|
33
|
+
> Full reference: [skills/unjwt/references/jws.md](./skills/unjwt/references/jws.md)
|
|
238
34
|
|
|
239
35
|
```ts
|
|
240
|
-
import {
|
|
241
|
-
import {
|
|
36
|
+
import { sign, verify } from "unjwt/jws";
|
|
37
|
+
import { generateJWK } from "unjwt/jwk";
|
|
242
38
|
|
|
243
|
-
|
|
244
|
-
const
|
|
39
|
+
// Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
|
|
40
|
+
const key = await generateJWK("HS256");
|
|
245
41
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
})
|
|
42
|
+
// Sign a JWT
|
|
43
|
+
const token = await sign(
|
|
44
|
+
{ sub: "user123", role: "admin" },
|
|
45
|
+
key,
|
|
46
|
+
{ expiresIn: "1h" }, // supports: number (seconds), "30s", "10m", "2h", "7D", "1Y"
|
|
47
|
+
);
|
|
251
48
|
|
|
252
|
-
|
|
253
|
-
|
|
49
|
+
// Verify and extract payload
|
|
50
|
+
const { payload, protectedHeader } = await verify(token, key);
|
|
51
|
+
console.log(payload); // { sub: "user123", role: "admin", iat: ..., exp: ... }
|
|
254
52
|
```
|
|
255
53
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
Decrypts a JWE token.
|
|
259
|
-
|
|
260
|
-
- `jwe`: The JWE token string.
|
|
261
|
-
- `key`: The Key Decryption Key (KDK) or password (`CryptoKey`, `JWK`, `string`, `Uint8Array`, or a `JWEKeyLookupFunction`).
|
|
262
|
-
A `JWEKeyLookupFunction` has the signature `(header: JWEHeaderParameters) => Promise<CryptoKey | JWK | string | Uint8Array> | CryptoKey | JWK | string | Uint8Array`.
|
|
263
|
-
- `options` (optional):
|
|
264
|
-
- `algorithms`: Array of allowed JWE Key Management `alg` values.
|
|
265
|
-
- `validateJWT`: Unless false, will parse payload as JWT and validate claims if applicable (typ includes "jwt", case insensitive). Default `undefined`.
|
|
266
|
-
- `encryptionAlgorithms`: Array of allowed JWE Content Encryption `enc` values.
|
|
267
|
-
- `critical`: Array of JWE header parameter names that the application understands.
|
|
268
|
-
- `unwrappedKeyAlgorithm`: (For `unwrapKey` internally) Algorithm details for the CEK after unwrapping.
|
|
269
|
-
- `keyUsage`: (For `unwrapKey` internally) Intended usages for the unwrapped CEK.
|
|
270
|
-
|
|
271
|
-
Returns a `Promise<JWEDecryptResult<T>>` which is an object `{ payload: T, protectedHeader: JWEHeaderParameters, cek: Uint8Array, aad: Uint8Array }`.
|
|
272
|
-
The `payload` type `T` can be `JWTClaims` (object) or `string`.
|
|
273
|
-
|
|
274
|
-
**Example (PBES2 password-based decryption):**
|
|
54
|
+
**Asymmetric keys (RS256, ES256, PS256, Ed25519, etc.):**
|
|
275
55
|
|
|
276
56
|
```ts
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const { payload } = await decrypt(jweToken, password);
|
|
57
|
+
const keys = await generateJWK("RS256");
|
|
58
|
+
const token = await sign({ sub: "user123" }, keys.privateKey);
|
|
59
|
+
const { payload } = await verify(token, keys.publicKey);
|
|
282
60
|
```
|
|
283
61
|
|
|
284
|
-
**
|
|
62
|
+
**Key lookup function for dynamic key resolution:**
|
|
285
63
|
|
|
286
64
|
```ts
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
encryptionAlgorithms: ["A128GCM"],
|
|
296
|
-
});
|
|
297
|
-
console.log("Decrypted Plaintext:", payload);
|
|
298
|
-
console.log("Protected Header:", protectedHeader);
|
|
299
|
-
// console.log("CEK (Content Encryption Key):", cek);
|
|
300
|
-
} catch (error) {
|
|
301
|
-
console.error("Decryption failed:", error);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
65
|
+
const { payload } = await verify(
|
|
66
|
+
token,
|
|
67
|
+
async (header) => {
|
|
68
|
+
// Resolve key by kid, alg, or any header field
|
|
69
|
+
return await fetchPublicKeyByKid(header.kid);
|
|
70
|
+
},
|
|
71
|
+
{ algorithms: ["RS256"] },
|
|
72
|
+
);
|
|
304
73
|
```
|
|
305
74
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
### JWK (JSON Web Key, [RFC 7517](https://datatracker.ietf.org/doc/html/rfc7517))
|
|
309
|
-
|
|
310
|
-
Utilities for working with JSON Web Keys.
|
|
311
|
-
|
|
312
|
-
#### `generateKey(alg, options?)`
|
|
313
|
-
|
|
314
|
-
Generates a cryptographic key.
|
|
75
|
+
### JWE — Encrypt & Decrypt
|
|
315
76
|
|
|
316
|
-
|
|
317
|
-
- `options` (optional):
|
|
318
|
-
- `toJWK`: If `true`, returns the key(s) in JWK format. Otherwise, returns `CryptoKey`(s) or `Uint8Array` (for composite keys like AES-CBC-HS\*). Default `false`.
|
|
319
|
-
- `extractable`: Boolean, whether the generated `CryptoKey` can be exported. Default `true`.
|
|
320
|
-
- `keyUsage`: Array of `KeyUsage` strings. Defaults are algorithm-specific.
|
|
321
|
-
- `modulusLength`: For RSA keys (e.g., `2048`, `4096`). Default `2048`.
|
|
322
|
-
- `publicExponent`: For RSA keys. Default `new Uint8Array([0x01, 0x00, 0x01])`.
|
|
323
|
-
|
|
324
|
-
Returns a `Promise` resolving to `CryptoKey`, `CryptoKeyPair`, `Uint8Array` (for composite keys), `JWK`, or `{ privateKey: JWK, publicKey: JWK }` depending on `alg` and `options.toJWK`.
|
|
325
|
-
|
|
326
|
-
**Examples:**
|
|
77
|
+
> Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
|
|
327
78
|
|
|
328
79
|
```ts
|
|
329
|
-
import {
|
|
330
|
-
|
|
331
|
-
// Generate an HS256 CryptoKey
|
|
332
|
-
const hmacKey = await generateKey("HS256");
|
|
333
|
-
console.log(hmacKey); // CryptoKey
|
|
334
|
-
|
|
335
|
-
// Generate an RS256 CryptoKeyPair
|
|
336
|
-
const rsaKeyPair = await generateKey("RS256", { modulusLength: 2048 });
|
|
337
|
-
console.log(rsaKeyPair.publicKey); // CryptoKey
|
|
338
|
-
console.log(rsaKeyPair.privateKey); // CryptoKey
|
|
339
|
-
|
|
340
|
-
// Generate an ES384 key pair as JWKs
|
|
341
|
-
const ecJwks = await generateKey("ES384", { toJWK: true });
|
|
342
|
-
console.log(ecJwks.publicKey); // JWK
|
|
343
|
-
console.log(ecJwks.privateKey); // JWK
|
|
344
|
-
|
|
345
|
-
// Generate a composite key for A128CBC-HS256 as Uint8Array
|
|
346
|
-
const aesCbcHsKeyBytes = await generateKey("A128CBC-HS256");
|
|
347
|
-
console.log(aesCbcHsKeyBytes); // Uint8Array (32 bytes: 16 for AES, 16 for HMAC)
|
|
80
|
+
import { encrypt, decrypt } from "unjwt/jwe";
|
|
348
81
|
|
|
349
|
-
//
|
|
350
|
-
const
|
|
351
|
-
|
|
82
|
+
// Password-based encryption (simplest — uses PBES2)
|
|
83
|
+
const token = await encrypt({ secret: "sensitive data" }, "my-password");
|
|
84
|
+
const { payload } = await decrypt(token, "my-password");
|
|
352
85
|
```
|
|
353
86
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
Derives a key from a password using PBKDF2 for PBES2 algorithms.
|
|
357
|
-
|
|
358
|
-
- `password`: The password (`string` or `Uint8Array`).
|
|
359
|
-
- `alg`: The PBES2 algorithm (e.g., `"PBES2-HS256+A128KW"`).
|
|
360
|
-
- `options`:
|
|
361
|
-
- `salt`: The salt (`Uint8Array`, at least 8 octets).
|
|
362
|
-
- `iterations`: The iteration count (positive integer).
|
|
363
|
-
- `toJWK`: If `true`, returns a `JWK_oct`. Otherwise, `CryptoKey`. Default `false`.
|
|
364
|
-
- `extractable`: Boolean for `CryptoKey`. Default `false` unless `toJWK` is true.
|
|
365
|
-
- `keyUsage`: For `CryptoKey`. Default `["wrapKey", "unwrapKey"]`.
|
|
366
|
-
|
|
367
|
-
Returns a `Promise` resolving to `CryptoKey` or `JWK_oct`.
|
|
368
|
-
|
|
369
|
-
**Example:**
|
|
87
|
+
**With explicit key management:**
|
|
370
88
|
|
|
371
89
|
```ts
|
|
372
|
-
import {
|
|
373
|
-
import { randomBytes, textEncoder } from "unjwt/utils";
|
|
90
|
+
import { generateJWK } from "unjwt/jwk";
|
|
374
91
|
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
92
|
+
// AES Key Wrap
|
|
93
|
+
const aesKey = await generateJWK("A256KW");
|
|
94
|
+
const token = await encrypt({ data: "secret" }, aesKey);
|
|
378
95
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
383
|
-
console.log(derivedKey); // CryptoKey for AES-KW (192-bit)
|
|
96
|
+
// RSA-OAEP
|
|
97
|
+
const rsaKeys = await generateJWK("RSA-OAEP-256");
|
|
98
|
+
const token2 = await encrypt({ data: "secret" }, rsaKeys.publicKey);
|
|
99
|
+
const { payload } = await decrypt(token2, rsaKeys.privateKey);
|
|
384
100
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
toJWK: true,
|
|
389
|
-
});
|
|
390
|
-
console.log(derivedJwk); // JWK_oct { kty: "oct", k: "...", alg: "A256KW" }
|
|
101
|
+
// ECDH-ES
|
|
102
|
+
const ecKeys = await generateJWK("ECDH-ES+A256KW");
|
|
103
|
+
const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
|
|
391
104
|
```
|
|
392
105
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
Imports a key from various formats. This is a flexible wrapper.
|
|
396
|
-
|
|
397
|
-
- `keyMaterial`: The key to import. Can be:
|
|
398
|
-
- `CryptoKey`: Returned directly.
|
|
399
|
-
- `Uint8Array`: Returned directly (treated as raw symmetric key bytes).
|
|
400
|
-
- `string`: Encoded to `Uint8Array` and returned.
|
|
401
|
-
- `JWK_oct` (symmetric JWK with `k` property): The `k` value is Base64URL decoded and returned as `Uint8Array`.
|
|
402
|
-
- Other `JWK` types (asymmetric): Imported into a `CryptoKey`.
|
|
403
|
-
- `alg` (optional): The JWA algorithm string. **Required** when importing asymmetric JWKs (e.g., RSA, EC) to provide context for `crypto.subtle.importKey`.
|
|
106
|
+
### JWK — Key Management
|
|
404
107
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
**Examples:**
|
|
108
|
+
> Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
|
|
408
109
|
|
|
409
110
|
```ts
|
|
410
|
-
import {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
k: "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr0", // Example key
|
|
422
|
-
};
|
|
423
|
-
const importedOctBytes = await importKey(octJwk); // Returns Uint8Array
|
|
424
|
-
console.log(importedOctBytes); // Uint8Array (decoded from k)
|
|
425
|
-
|
|
426
|
-
// Import an RSA Public Key JWK
|
|
427
|
-
const rsaPublicJwk = {
|
|
428
|
-
kty: "RSA",
|
|
429
|
-
n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3ok92YEnjsADC4Ue87zwRzH2J-TCwlcQrY3E9gGZJZL2g_2_5QjLhL0gR0xYj04_N4M",
|
|
430
|
-
e: "AQAB",
|
|
431
|
-
alg: "RS256",
|
|
432
|
-
kid: "rsa-pub-1",
|
|
433
|
-
};
|
|
434
|
-
const rsaPublicKey = await importKey(rsaPublicJwk, "RS256"); // 'alg' is crucial here
|
|
435
|
-
console.log(rsaPublicKey); // CryptoKey
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
#### `exportKey(key, jwk?)`
|
|
439
|
-
|
|
440
|
-
Exports a `CryptoKey` to JWK format.
|
|
441
|
-
|
|
442
|
-
- `key`: The `CryptoKey` to export (must be `extractable`).
|
|
443
|
-
- `jwk` (optional): A partial `JWK` object to merge with the exported properties (e.g., to add `kid`, `use`, or override `alg`).
|
|
111
|
+
import {
|
|
112
|
+
generateKey,
|
|
113
|
+
generateJWK,
|
|
114
|
+
importKey,
|
|
115
|
+
exportKey,
|
|
116
|
+
importJWKFromPEM,
|
|
117
|
+
exportJWKToPEM,
|
|
118
|
+
wrapKey,
|
|
119
|
+
unwrapKey,
|
|
120
|
+
deriveKeyFromPassword,
|
|
121
|
+
} from "unjwt/jwk";
|
|
444
122
|
|
|
445
|
-
|
|
123
|
+
// Generate keys as CryptoKey or JWK
|
|
124
|
+
const hmacKey = await generateKey("HS256"); // CryptoKey
|
|
125
|
+
const rsaPair = await generateKey("RS256"); // CryptoKeyPair
|
|
126
|
+
const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
|
|
446
127
|
|
|
447
|
-
|
|
128
|
+
// PEM conversion
|
|
129
|
+
const jwk = await importJWKFromPEM(pemString, "spki", "RS256", undefined, { kid: "rsa-1" });
|
|
130
|
+
const pem = await exportJWKToPEM(jwk, "spki");
|
|
448
131
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
const jwk = await exportKey(publicKey, { kid: "ec-key-001", use: "sig" });
|
|
455
|
-
console.log(jwk);
|
|
456
|
-
// {
|
|
457
|
-
// kty: 'EC',
|
|
458
|
-
// crv: 'P-256',
|
|
459
|
-
// x: '...',
|
|
460
|
-
// y: '...',
|
|
461
|
-
// ext: true,
|
|
462
|
-
// key_ops: [ 'verify' ], // or as per generation
|
|
463
|
-
// kid: 'ec-key-001',
|
|
464
|
-
// use: 'sig'
|
|
465
|
-
// }
|
|
132
|
+
// Key wrapping
|
|
133
|
+
const cek = crypto.getRandomValues(new Uint8Array(32));
|
|
134
|
+
const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
|
|
135
|
+
const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { returnAs: false });
|
|
466
136
|
```
|
|
467
137
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
Wraps a Content Encryption Key (CEK).
|
|
471
|
-
|
|
472
|
-
- `alg`: The JWA key management algorithm (e.g., `"A128KW"`, `"RSA-OAEP"`).
|
|
473
|
-
- `keyToWrap`: The CEK to wrap (`CryptoKey` or `Uint8Array`).
|
|
474
|
-
- `wrappingKey`: The Key Encryption Key (KEK) (`CryptoKey`, `JWK`, or password `string`/`Uint8Array` for PBES2).
|
|
475
|
-
- `options` (optional): Algorithm-specific options (e.g., `p2s`, `p2c` for PBES2; `iv` for AES-GCMKW).
|
|
476
|
-
|
|
477
|
-
Returns a `Promise<WrapKeyResult>` containing `encryptedKey` and other parameters like `iv`, `tag`, `epk`, `p2s`, `p2c` as needed by the algorithm.
|
|
138
|
+
### Utility Functions
|
|
478
139
|
|
|
479
|
-
|
|
140
|
+
> Full reference: [skills/unjwt/references/utils.md](./skills/unjwt/references/utils.md)
|
|
480
141
|
|
|
481
142
|
```ts
|
|
482
|
-
import {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
143
|
+
import {
|
|
144
|
+
base64UrlEncode,
|
|
145
|
+
base64UrlDecode,
|
|
146
|
+
randomBytes,
|
|
147
|
+
isJWK,
|
|
148
|
+
isJWKSet,
|
|
149
|
+
isSymmetricJWK,
|
|
150
|
+
isPrivateJWK,
|
|
151
|
+
isPublicJWK,
|
|
152
|
+
isCryptoKey,
|
|
153
|
+
textEncoder,
|
|
154
|
+
textDecoder,
|
|
155
|
+
} from "unjwt/utils";
|
|
487
156
|
|
|
488
|
-
const
|
|
489
|
-
|
|
157
|
+
const bytes = randomBytes(32);
|
|
158
|
+
const encoded = base64UrlEncode(bytes);
|
|
159
|
+
const decoded = base64UrlDecode(encoded, false); // Uint8Array
|
|
490
160
|
```
|
|
491
161
|
|
|
492
|
-
|
|
162
|
+
### H3 Session Adapters
|
|
493
163
|
|
|
494
|
-
|
|
164
|
+
> Full reference: [skills/unjwt/references/adapters-h3.md](./skills/unjwt/references/adapters-h3.md)
|
|
495
165
|
|
|
496
|
-
-
|
|
497
|
-
- `wrappedKey`: The encrypted CEK (`Uint8Array`).
|
|
498
|
-
- `unwrappingKey`: The Key Decryption Key (KDK).
|
|
499
|
-
- `options` (optional):
|
|
500
|
-
- `returnAs`: If `false`, returns `Uint8Array`. If `true` (default) or undefined, returns `CryptoKey`.
|
|
501
|
-
- `unwrappedKeyAlgorithm`: `AlgorithmIdentifier` for the imported CEK if `returnAs` is `true`.
|
|
502
|
-
- `keyUsage`: `KeyUsage[]` for the imported CEK if `returnAs` is `true`.
|
|
503
|
-
- `extractable`: Boolean for the imported CEK.
|
|
504
|
-
- Other algorithm-specific options (e.g., `p2s`, `p2c`, `iv`, `tag`, `epk`).
|
|
166
|
+
Cookie-based JWT session management for H3 applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies.
|
|
505
167
|
|
|
506
|
-
|
|
168
|
+
> **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.
|
|
507
169
|
|
|
508
|
-
|
|
170
|
+
#### JWE Session (encrypted, recommended for sensitive data)
|
|
509
171
|
|
|
510
172
|
```ts
|
|
511
|
-
import {
|
|
512
|
-
// const encryptedKey = ...; // From wrapKey example
|
|
513
|
-
// const kdk = ...; // Same KEK used for wrapping
|
|
173
|
+
import { useJWESession, generateJWK } from "unjwt/adapters/h3v2";
|
|
514
174
|
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
175
|
+
app.get("/profile", async (event) => {
|
|
176
|
+
const session = await useJWESession(event, {
|
|
177
|
+
key: process.env.SESSION_SECRET!, // password string, symmetric JWK, or asymmetric keypair
|
|
178
|
+
maxAge: "7D",
|
|
518
179
|
});
|
|
519
|
-
console.log("Unwrapped CEK (bytes):", unwrappedCekBytes); // Uint8Array
|
|
520
|
-
|
|
521
|
-
const unwrappedCekCryptoKey = await unwrapKey("A128KW", encryptedKey, kdk, {
|
|
522
|
-
returnAs: true, // Get CryptoKey
|
|
523
|
-
unwrappedKeyAlgorithm: { name: "AES-GCM", length: 256 }, // Specify CEK's intended alg
|
|
524
|
-
keyUsage: ["encrypt", "decrypt"],
|
|
525
|
-
});
|
|
526
|
-
console.log("Unwrapped CEK (CryptoKey):", unwrappedCekCryptoKey); // CryptoKey
|
|
527
|
-
}
|
|
528
|
-
```
|
|
529
180
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
- `pem`: The PEM-encoded string (including `-----BEGIN ...-----` and `-----END ...-----` markers).
|
|
535
|
-
- `pemType`: The type of PEM encoding:
|
|
536
|
-
- `"pkcs8"`: For private keys in PKCS#8 format.
|
|
537
|
-
- `"spki"`: For public keys in SPKI format.
|
|
538
|
-
- `"x509"`: For X.509 certificates (extracts the public key).
|
|
539
|
-
- `alg`: The JWA algorithm identifier (e.g., `"RS256"`, `"ES256"`). This is crucial for `crypto.subtle.importKey` to understand the key's intended algorithm and for setting the `'alg'` field in the resulting JWK.
|
|
540
|
-
- `importOptions` (optional): Options for the underlying `crypto.subtle.importKey` call:
|
|
541
|
-
- `extractable`: Boolean, whether the imported `CryptoKey` should be extractable. Defaults to `true`.
|
|
542
|
-
- `keyUsage`: Array of `KeyUsage` strings for the imported `CryptoKey`.
|
|
543
|
-
- `jwkExtras` (optional): An object containing additional properties to merge into the resulting JWK (e.g., `"kid"`, `"use"`).
|
|
544
|
-
|
|
545
|
-
Returns a `Promise<JWK>` resolving to the imported key as a JWK.
|
|
546
|
-
|
|
547
|
-
**Example:**
|
|
548
|
-
|
|
549
|
-
```ts
|
|
550
|
-
import { importJWKFromPEM } from "unjwt/jwk";
|
|
551
|
-
|
|
552
|
-
const rsaPublicJwk = await importJWKFromPEM(
|
|
553
|
-
provess.env.RSA_PEM_SPKI, // PEM string
|
|
554
|
-
"spki",
|
|
555
|
-
"RS256",
|
|
556
|
-
{ extractable: false },
|
|
557
|
-
{ kid: "my-rsa-key" }, // Additional properties to add to the JWK
|
|
558
|
-
);
|
|
559
|
-
console.log(rsaPublicJwk);
|
|
560
|
-
// {
|
|
561
|
-
// kty: 'RSA',
|
|
562
|
-
// alg: 'RS256',
|
|
563
|
-
// kid: 'my-rsa-key',
|
|
564
|
-
// n: '...',
|
|
565
|
-
// e: 'AQAB',
|
|
566
|
-
// ext: false,
|
|
567
|
-
// key_ops: [ 'verify' ]
|
|
568
|
-
// }
|
|
569
|
-
```
|
|
570
|
-
|
|
571
|
-
#### exportJWKToPEM(jwk, pemFormat, algForCryptoKeyImport?)
|
|
572
|
-
|
|
573
|
-
Exports a JWK to a PEM-encoded string.
|
|
574
|
-
|
|
575
|
-
- jwk: The JWK to export.
|
|
576
|
-
- pemFormat: The desired PEM format:
|
|
577
|
-
- "pkcs8": For private keys in PKCS#8 format.
|
|
578
|
-
- "spki": For public keys in SPKI format.
|
|
579
|
-
- algForCryptoKeyImport (optional): If the JWK does not have an 'alg' property, this algorithm hint is required to correctly convert it to a CryptoKey first. This is only needed if the JWK lacks an alg property.
|
|
580
|
-
|
|
581
|
-
Returns a `Promise<string>` resolving to the PEM-encoded key string.
|
|
582
|
-
|
|
583
|
-
Example:
|
|
181
|
+
if (!session.id) {
|
|
182
|
+
// No active session — create one
|
|
183
|
+
await session.update({ userId: "123", email: "user@example.com" });
|
|
184
|
+
}
|
|
584
185
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
import { rsaJWK } from "./keys"; // Assuming you have JWKs in keys.ts
|
|
588
|
-
|
|
589
|
-
const rsaPrivatePem = await exportJWKToPEM(rsaJWK.private, "pkcs8");
|
|
590
|
-
console.log(rsaPrivatePem);
|
|
591
|
-
// -----BEGIN PRIVATE KEY-----
|
|
592
|
-
// MII...
|
|
593
|
-
// -----END PRIVATE KEY-----
|
|
594
|
-
|
|
595
|
-
const rsaPublicSpki = await exportJWKToPEM(
|
|
596
|
-
rsaJWK.public,
|
|
597
|
-
"spki",
|
|
598
|
-
"RS256", // this is required if `rsaJWK.public.alg` is undefined
|
|
599
|
-
);
|
|
600
|
-
console.log(rsaPublicSpki);
|
|
601
|
-
// -----BEGIN PUBLIC KEY-----
|
|
602
|
-
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...
|
|
603
|
-
// -----END PUBLIC KEY-----
|
|
186
|
+
return { user: session.data };
|
|
187
|
+
});
|
|
604
188
|
```
|
|
605
189
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
> [!WARNING]
|
|
609
|
-
> While these adapters try to be compatible with h3's `useSession` API, the key difference is that new sessions are not automatically created when calling `useJWESession` or `useJWSSession`, marking their `id` as `undefined` until you explicitly call `session.update()`. This is to comply with various specs that do integrate JWT one (such as OAuth) where sessions are only created upon valid operations (like user authorization).
|
|
610
|
-
|
|
611
|
-
#### H3 v1 (Nuxt v4, Nitro v2)
|
|
612
|
-
|
|
613
|
-
The `h3v1` adapter bundles session helpers that store data inside signed or encrypted JWTs.
|
|
614
|
-
|
|
615
|
-
- `useJWESession(event, config)` encrypts the session payload with the provided `secret` (password string or private/symmetric JWK). Use this when session data must remain confidential. (cookie's `httpOnly: true` by default)
|
|
616
|
-
- `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
|
|
617
|
-
|
|
618
|
-
Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
|
|
190
|
+
#### JWS Session (signed, readable by client)
|
|
619
191
|
|
|
620
192
|
```ts
|
|
621
|
-
import {
|
|
622
|
-
import { useJWESession, useJWSSession, generateJWK } from "unjwt/adapters/h3v1";
|
|
623
|
-
|
|
624
|
-
const keys = await generateJWK("RS256"); // Make sure to persist these keys somewhere!
|
|
625
|
-
|
|
626
|
-
export default defineEventHandler(async (event) => {
|
|
627
|
-
const privateSession = await useJWESession(event, {
|
|
628
|
-
name: "app-session",
|
|
629
|
-
secret: process.env.SESSION_SECRET!, // or symmetric or asymmetric keypair
|
|
630
|
-
});
|
|
193
|
+
import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";
|
|
631
194
|
|
|
632
|
-
|
|
633
|
-
visits: (data.visits ?? 0) + 1,
|
|
634
|
-
}));
|
|
195
|
+
const keys = await generateJWK("RS256"); // persist these!
|
|
635
196
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
key: keys,
|
|
639
|
-
maxAge:
|
|
197
|
+
app.get("/preferences", async (event) => {
|
|
198
|
+
const session = await useJWSSession(event, {
|
|
199
|
+
key: keys,
|
|
200
|
+
maxAge: "24h",
|
|
640
201
|
});
|
|
641
202
|
|
|
642
|
-
return {
|
|
643
|
-
encryptedSession: privateSession.data,
|
|
644
|
-
signedSession: publicSession.data,
|
|
645
|
-
};
|
|
203
|
+
return { theme: session.data.theme };
|
|
646
204
|
});
|
|
647
205
|
```
|
|
648
206
|
|
|
649
|
-
####
|
|
207
|
+
#### Session features
|
|
650
208
|
|
|
651
|
-
|
|
209
|
+
- **Cookie chunking** — large tokens are automatically split across multiple cookies
|
|
210
|
+
- **Header-based tokens** — read from `Authorization: Bearer <token>` or custom headers via `sessionHeader`
|
|
211
|
+
- **Lifecycle hooks** — `onRead`, `onUpdate`, `onClear`, `onExpire`, `onError` for logging, refresh logic, etc.
|
|
212
|
+
- **Key lookup hooks** — `onUnsealKeyLookup` (JWE) / `onVerifyKeyLookup` (JWS) for key rotation
|
|
213
|
+
- **TypeScript generics** — strongly-typed session data via `useJWESession<MyData>(event, config)`
|
|
652
214
|
|
|
653
|
-
|
|
654
|
-
- `useJWSSession(event, config)` signs, but does not encrypt, the session payload with `config.key`. Use this when clients may read the session content but you still need tamper protection. (cookie's `httpOnly: false` by default)
|
|
655
|
-
|
|
656
|
-
Both helpers expose the same API: read `session.id` / `session.data`, call `session.update()` to patch values, and `session.clear()` to invalidate the cookie. When setting `maxAge`, `exp` claim is automatically managed and validated. They also support a number of `hooks` in the config for custom logic (e.g. logging, refreshing, etc).
|
|
215
|
+
#### Refresh token pattern
|
|
657
216
|
|
|
658
217
|
```ts
|
|
659
|
-
import { H3, HTTPError, serve } from "h3v2";
|
|
660
|
-
|
|
661
218
|
import {
|
|
662
219
|
type SessionConfigJWE,
|
|
663
220
|
type SessionConfigJWS,
|
|
@@ -668,112 +225,31 @@ import {
|
|
|
668
225
|
generateJWK,
|
|
669
226
|
} from "unjwt/adapters/h3v2";
|
|
670
227
|
|
|
671
|
-
const
|
|
228
|
+
const atKeys = await generateJWK("RS256");
|
|
672
229
|
|
|
673
|
-
const
|
|
674
|
-
key:
|
|
230
|
+
const refreshConfig = {
|
|
231
|
+
key: process.env.REFRESH_SECRET!,
|
|
675
232
|
name: "refresh_token",
|
|
676
233
|
} satisfies SessionConfigJWE;
|
|
677
234
|
|
|
678
|
-
const
|
|
679
|
-
key:
|
|
235
|
+
const accessConfig = {
|
|
236
|
+
key: atKeys,
|
|
680
237
|
name: "access_token",
|
|
681
|
-
maxAge:
|
|
238
|
+
maxAge: "15m",
|
|
682
239
|
hooks: {
|
|
683
240
|
async onExpire({ event, config }) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
241
|
+
const refresh = await getJWESession(event, refreshConfig);
|
|
242
|
+
if (refresh.data.sub) {
|
|
243
|
+
await updateJWSSession(event, config, {
|
|
244
|
+
sub: refresh.data.sub,
|
|
245
|
+
scope: refresh.data.scope,
|
|
246
|
+
});
|
|
688
247
|
}
|
|
689
|
-
|
|
690
|
-
console.log("Access token expired, refreshing...");
|
|
691
|
-
|
|
692
|
-
// refresh the access token
|
|
693
|
-
await updateJWSSession(event, config, {
|
|
694
|
-
sub: refreshSession.data.sub,
|
|
695
|
-
scope: refreshSession.data.scope,
|
|
696
|
-
});
|
|
697
248
|
},
|
|
698
249
|
},
|
|
699
250
|
} satisfies SessionConfigJWS;
|
|
700
|
-
|
|
701
|
-
const app = new H3();
|
|
702
|
-
|
|
703
|
-
app.post("/login", async (event) => {
|
|
704
|
-
const refreshSession = await useJWESession(event, jweOptions);
|
|
705
|
-
const accessSession = await useJWSSession(event, jwsOptions);
|
|
706
|
-
|
|
707
|
-
if (accessSession.data.sub) {
|
|
708
|
-
// user already logged in, return existing info
|
|
709
|
-
return {
|
|
710
|
-
accessToken: {
|
|
711
|
-
id: accessSession.id,
|
|
712
|
-
createdAt: accessSession.createdAt,
|
|
713
|
-
expiresAt: accessSession.expiresAt,
|
|
714
|
-
data: accessSession.data,
|
|
715
|
-
},
|
|
716
|
-
refreshSession: {
|
|
717
|
-
id: refreshSession.id,
|
|
718
|
-
createdAt: refreshSession.createdAt,
|
|
719
|
-
expiresAt: refreshSession.expiresAt,
|
|
720
|
-
data: refreshSession.data,
|
|
721
|
-
},
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const data = (await event.req.json()) as {
|
|
726
|
-
username?: string;
|
|
727
|
-
password?: string;
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
if (!data.username || !data.password) {
|
|
731
|
-
throw new HTTPError("Username and password are required", { status: 400 });
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// validate user credentials here
|
|
735
|
-
|
|
736
|
-
const claims = {
|
|
737
|
-
sub: data.username,
|
|
738
|
-
scope: ["read:profile"],
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
await accessSession.update(claims);
|
|
742
|
-
|
|
743
|
-
await refreshSession.update(claims);
|
|
744
|
-
|
|
745
|
-
return {
|
|
746
|
-
accessToken: {
|
|
747
|
-
id: accessSession.id,
|
|
748
|
-
createdAt: accessSession.createdAt,
|
|
749
|
-
expiresAt: accessSession.expiresAt,
|
|
750
|
-
data: accessSession.data,
|
|
751
|
-
},
|
|
752
|
-
refreshSession: {
|
|
753
|
-
id: refreshSession.id,
|
|
754
|
-
createdAt: refreshSession.createdAt,
|
|
755
|
-
expiresAt: refreshSession.expiresAt,
|
|
756
|
-
data: refreshSession.data,
|
|
757
|
-
},
|
|
758
|
-
};
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
serve(app);
|
|
762
251
|
```
|
|
763
252
|
|
|
764
|
-
---
|
|
765
|
-
|
|
766
|
-
### Utility Functions
|
|
767
|
-
|
|
768
|
-
`unjwt/utils` exports several helpful functions:
|
|
769
|
-
|
|
770
|
-
- `base64UrlEncode(data: Uint8Array | string): string`
|
|
771
|
-
- `base64UrlDecode(str?: string, toString?: boolean): Uint8Array | string` (Decodes to string by default, or `Uint8Array` if `toString` is `false`)
|
|
772
|
-
- `randomBytes(length: number): Uint8Array`
|
|
773
|
-
- `textEncoder: TextEncoder`
|
|
774
|
-
- `textDecoder: TextDecoder`
|
|
775
|
-
- Type guards: `isJWK(key)`, `isCryptoKey(key)`, `isCryptoKeyPair(keyPair)`
|
|
776
|
-
|
|
777
253
|
## Development
|
|
778
254
|
|
|
779
255
|
<details>
|
|
@@ -784,7 +260,7 @@ serve(app);
|
|
|
784
260
|
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
|
|
785
261
|
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
|
786
262
|
- Install dependencies using `pnpm install`
|
|
787
|
-
- Run
|
|
263
|
+
- Run tests using `pnpm test`
|
|
788
264
|
|
|
789
265
|
</details>
|
|
790
266
|
|