unjwt 0.5.38 → 0.5.40
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 +148 -670
- package/dist/_chunks/chunk.mjs +13 -0
- package/dist/_chunks/index.d.mts +558 -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 +611 -0
- package/dist/_chunks/jwk.mjs +1592 -0
- package/dist/_chunks/jws.d.mts +242 -0
- package/dist/_chunks/jws.mjs +2070 -0
- package/dist/_chunks/jws2.mjs +598 -0
- package/dist/_chunks/utils.mjs +375 -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 -147
- package/dist/adapters/h3v2.mjs +501 -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 +2 -3
- package/dist/jwk.d.mts +3 -2
- package/dist/jwk.mjs +2 -2
- package/dist/jws.d.mts +3 -3
- package/dist/jws.mjs +2 -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
|
@@ -1,663 +1,222 @@
|
|
|
1
1
|
# unjwt
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
[](https://npm.chart.dev/unjwt)
|
|
7
|
-
[](https://bundlephobia.com/package/unjwt)
|
|
8
|
-
|
|
9
|
-
<!-- /automd -->
|
|
3
|
+
[](https://npmx.dev/package/unjwt)
|
|
4
|
+
[](https://npmx.dev/package/unjwt)
|
|
5
|
+
[](https://npmx.dev/package/unjwt)
|
|
10
6
|
|
|
11
7
|
A collection of low-level JWT ([RFC 7519](https://datatracker.ietf.org/doc/html/rfc7519)) utilities using the Web Crypto API. Supports:
|
|
12
8
|
|
|
13
|
-
- **JWS (
|
|
14
|
-
- **JWE (
|
|
15
|
-
- **JWK (
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
- [H3
|
|
19
|
-
- [H3 v2 (Nuxt v5, Nitro v3)](./src/adapters/h3v2/): `useJWSSession()`, `useJWESession()`
|
|
9
|
+
- **JWS** ([RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515)) — sign and verify tokens using HMAC, RSA, RSA-PSS, ECDSA, and EdDSA algorithms
|
|
10
|
+
- **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
|
|
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)
|
|
20
15
|
|
|
21
|
-
|
|
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)
|
|
34
|
-
|
|
35
|
-
## Usage
|
|
16
|
+
Zero runtime dependencies for core. Optional peer deps for adapters (`h3`, `cookie-es`, `rou3`).
|
|
36
17
|
|
|
37
|
-
Install
|
|
18
|
+
## Install
|
|
38
19
|
|
|
39
20
|
```sh
|
|
40
|
-
#
|
|
21
|
+
# Auto-detect package manager (npm, yarn, pnpm, deno, bun)
|
|
41
22
|
npx nypm install unjwt
|
|
42
23
|
```
|
|
43
24
|
|
|
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
|
-
```
|
|
25
|
+
If you would like to install the agent skill you can run:
|
|
73
26
|
|
|
74
|
-
|
|
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"] }
|
|
27
|
+
```sh
|
|
28
|
+
npx skills add sandros94/unjwt
|
|
199
29
|
```
|
|
200
30
|
|
|
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!";
|
|
31
|
+
## Usage
|
|
230
32
|
|
|
231
|
-
|
|
232
|
-
const jweToken = await encrypt(plaintext, password);
|
|
233
|
-
console.log(jweToken);
|
|
234
|
-
// JWE token string...
|
|
235
|
-
```
|
|
33
|
+
### JWS — Sign & Verify
|
|
236
34
|
|
|
237
|
-
|
|
35
|
+
> Full reference: [skills/unjwt/references/jws.md](./skills/unjwt/references/jws.md)
|
|
238
36
|
|
|
239
37
|
```ts
|
|
240
|
-
import {
|
|
241
|
-
import {
|
|
38
|
+
import { sign, verify } from "unjwt/jws";
|
|
39
|
+
import { generateJWK } from "unjwt/jwk";
|
|
242
40
|
|
|
243
|
-
|
|
244
|
-
const
|
|
41
|
+
// Generate a key (HMAC, RSA, ECDSA, EdDSA, etc.)
|
|
42
|
+
const key = await generateJWK("HS256");
|
|
245
43
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
})
|
|
44
|
+
// Sign a JWT
|
|
45
|
+
const token = await sign(
|
|
46
|
+
{ sub: "user123", role: "admin" },
|
|
47
|
+
key,
|
|
48
|
+
{ expiresIn: "1h" }, // supports: number (seconds), "30s", "10m", "2h", "7D", "1Y"
|
|
49
|
+
);
|
|
251
50
|
|
|
252
|
-
|
|
253
|
-
|
|
51
|
+
// Verify and extract payload
|
|
52
|
+
const { payload, protectedHeader } = await verify(token, key);
|
|
53
|
+
console.log(payload); // { sub: "user123", role: "admin", iat: ..., exp: ... }
|
|
254
54
|
```
|
|
255
55
|
|
|
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):**
|
|
56
|
+
**Asymmetric keys (RS256, ES256, PS256, Ed25519, etc.):**
|
|
275
57
|
|
|
276
58
|
```ts
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const { payload } = await decrypt(jweToken, password);
|
|
59
|
+
const keys = await generateJWK("RS256");
|
|
60
|
+
const token = await sign({ sub: "user123" }, keys.privateKey);
|
|
61
|
+
const { payload } = await verify(token, keys.publicKey);
|
|
282
62
|
```
|
|
283
63
|
|
|
284
|
-
**
|
|
64
|
+
**Key lookup function for dynamic key resolution:**
|
|
285
65
|
|
|
286
66
|
```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
|
-
}
|
|
67
|
+
const { payload } = await verify(
|
|
68
|
+
token,
|
|
69
|
+
async (header) => {
|
|
70
|
+
// Resolve key by kid, alg, or any header field
|
|
71
|
+
return await fetchPublicKeyByKid(header.kid);
|
|
72
|
+
},
|
|
73
|
+
{ algorithms: ["RS256"] },
|
|
74
|
+
);
|
|
304
75
|
```
|
|
305
76
|
|
|
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.
|
|
77
|
+
### JWE — Encrypt & Decrypt
|
|
311
78
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
Generates a cryptographic key.
|
|
315
|
-
|
|
316
|
-
- `alg`: The JWA algorithm identifier for the key to be generated (e.g., `"HS256"`, `"RS256"`, `"ES256"`, `"A128KW"`, `"A128GCM"`, `"A128CBC-HS256"`).
|
|
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:**
|
|
79
|
+
> Full reference: [skills/unjwt/references/jwe.md](./skills/unjwt/references/jwe.md)
|
|
327
80
|
|
|
328
81
|
```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)
|
|
82
|
+
import { encrypt, decrypt } from "unjwt/jwe";
|
|
348
83
|
|
|
349
|
-
//
|
|
350
|
-
const
|
|
351
|
-
|
|
84
|
+
// Password-based encryption (simplest — uses PBES2)
|
|
85
|
+
const token = await encrypt({ secret: "sensitive data" }, "my-password");
|
|
86
|
+
const { payload } = await decrypt(token, "my-password");
|
|
352
87
|
```
|
|
353
88
|
|
|
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:**
|
|
89
|
+
**With explicit key management:**
|
|
370
90
|
|
|
371
91
|
```ts
|
|
372
|
-
import {
|
|
373
|
-
import { randomBytes, textEncoder } from "unjwt/utils";
|
|
92
|
+
import { generateJWK } from "unjwt/jwk";
|
|
374
93
|
|
|
375
|
-
|
|
376
|
-
const
|
|
377
|
-
const
|
|
94
|
+
// AES Key Wrap
|
|
95
|
+
const aesKey = await generateJWK("A256KW");
|
|
96
|
+
const token = await encrypt({ data: "secret" }, aesKey);
|
|
378
97
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
});
|
|
383
|
-
console.log(derivedKey); // CryptoKey for AES-KW (192-bit)
|
|
98
|
+
// RSA-OAEP
|
|
99
|
+
const rsaKeys = await generateJWK("RSA-OAEP-256");
|
|
100
|
+
const token2 = await encrypt({ data: "secret" }, rsaKeys.publicKey);
|
|
101
|
+
const { payload } = await decrypt(token2, rsaKeys.privateKey);
|
|
384
102
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
toJWK: true,
|
|
389
|
-
});
|
|
390
|
-
console.log(derivedJwk); // JWK_oct { kty: "oct", k: "...", alg: "A256KW" }
|
|
103
|
+
// ECDH-ES
|
|
104
|
+
const ecKeys = await generateJWK("ECDH-ES+A256KW");
|
|
105
|
+
const token3 = await encrypt({ data: "secret" }, ecKeys.publicKey);
|
|
391
106
|
```
|
|
392
107
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
Imports a key from various formats. This is a flexible wrapper.
|
|
108
|
+
### JWK — Key Management
|
|
396
109
|
|
|
397
|
-
|
|
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`.
|
|
404
|
-
|
|
405
|
-
Returns a `Promise` resolving to `CryptoKey` or `Uint8Array`.
|
|
406
|
-
|
|
407
|
-
**Examples:**
|
|
110
|
+
> Full reference: [skills/unjwt/references/jwk.md](./skills/unjwt/references/jwk.md)
|
|
408
111
|
|
|
409
112
|
```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`).
|
|
113
|
+
import {
|
|
114
|
+
generateKey,
|
|
115
|
+
generateJWK,
|
|
116
|
+
importKey,
|
|
117
|
+
exportKey,
|
|
118
|
+
importJWKFromPEM,
|
|
119
|
+
exportJWKToPEM,
|
|
120
|
+
wrapKey,
|
|
121
|
+
unwrapKey,
|
|
122
|
+
deriveKeyFromPassword,
|
|
123
|
+
} from "unjwt/jwk";
|
|
444
124
|
|
|
445
|
-
|
|
125
|
+
// Generate keys as CryptoKey or JWK
|
|
126
|
+
const hmacKey = await generateKey("HS256"); // CryptoKey
|
|
127
|
+
const rsaPair = await generateKey("RS256"); // CryptoKeyPair
|
|
128
|
+
const ecJwk = await generateJWK("ES256", { kid: "k1" }); // { privateKey: JWK, publicKey: JWK }
|
|
446
129
|
|
|
447
|
-
|
|
130
|
+
// PEM conversion
|
|
131
|
+
const jwk = await importJWKFromPEM(pemString, "spki", "RS256", undefined, { kid: "rsa-1" });
|
|
132
|
+
const pem = await exportJWKToPEM(jwk, "spki");
|
|
448
133
|
|
|
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
|
-
// }
|
|
134
|
+
// Key wrapping
|
|
135
|
+
const cek = crypto.getRandomValues(new Uint8Array(32));
|
|
136
|
+
const { encryptedKey } = await wrapKey("A256KW", cek, aesKey);
|
|
137
|
+
const unwrapped = await unwrapKey("A256KW", encryptedKey, aesKey, { returnAs: false });
|
|
466
138
|
```
|
|
467
139
|
|
|
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.
|
|
140
|
+
### Utility Functions
|
|
478
141
|
|
|
479
|
-
|
|
142
|
+
> Full reference: [skills/unjwt/references/utils.md](./skills/unjwt/references/utils.md)
|
|
480
143
|
|
|
481
144
|
```ts
|
|
482
|
-
import {
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
145
|
+
import {
|
|
146
|
+
base64UrlEncode,
|
|
147
|
+
base64UrlDecode,
|
|
148
|
+
randomBytes,
|
|
149
|
+
isJWK,
|
|
150
|
+
isJWKSet,
|
|
151
|
+
isSymmetricJWK,
|
|
152
|
+
isPrivateJWK,
|
|
153
|
+
isPublicJWK,
|
|
154
|
+
isCryptoKey,
|
|
155
|
+
textEncoder,
|
|
156
|
+
textDecoder,
|
|
157
|
+
} from "unjwt/utils";
|
|
487
158
|
|
|
488
|
-
const
|
|
489
|
-
|
|
159
|
+
const bytes = randomBytes(32);
|
|
160
|
+
const encoded = base64UrlEncode(bytes);
|
|
161
|
+
const decoded = base64UrlDecode(encoded, false); // Uint8Array
|
|
490
162
|
```
|
|
491
163
|
|
|
492
|
-
|
|
164
|
+
### H3 Session Adapters
|
|
493
165
|
|
|
494
|
-
|
|
166
|
+
> Full reference: [skills/unjwt/references/adapters-h3.md](./skills/unjwt/references/adapters-h3.md)
|
|
495
167
|
|
|
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`).
|
|
168
|
+
Cookie-based JWT session management for H3 applications. Sessions are stored as encrypted (JWE) or signed (JWS) tokens in chunked cookies.
|
|
505
169
|
|
|
506
|
-
|
|
170
|
+
> **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
171
|
|
|
508
|
-
|
|
172
|
+
#### JWE Session (encrypted, recommended for sensitive data)
|
|
509
173
|
|
|
510
174
|
```ts
|
|
511
|
-
import {
|
|
512
|
-
// const encryptedKey = ...; // From wrapKey example
|
|
513
|
-
// const kdk = ...; // Same KEK used for wrapping
|
|
175
|
+
import { useJWESession, generateJWK } from "unjwt/adapters/h3v2";
|
|
514
176
|
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
|
|
177
|
+
app.get("/profile", async (event) => {
|
|
178
|
+
const session = await useJWESession(event, {
|
|
179
|
+
key: process.env.SESSION_SECRET!, // password string, symmetric JWK, or asymmetric keypair
|
|
180
|
+
maxAge: "7D",
|
|
518
181
|
});
|
|
519
|
-
console.log("Unwrapped CEK (bytes):", unwrappedCekBytes); // Uint8Array
|
|
520
182
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
});
|
|
526
|
-
console.log("Unwrapped CEK (CryptoKey):", unwrappedCekCryptoKey); // CryptoKey
|
|
527
|
-
}
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
#### `importJWKFromPEM(pem, pemType, alg, importOptions?, jwkExtras?)`
|
|
531
|
-
|
|
532
|
-
Imports a key from a PEM-encoded string and converts it to a JWK.
|
|
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:
|
|
183
|
+
if (!session.id) {
|
|
184
|
+
// No active session — create one
|
|
185
|
+
await session.update({ userId: "123", email: "user@example.com" });
|
|
186
|
+
}
|
|
584
187
|
|
|
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-----
|
|
188
|
+
return { user: session.data };
|
|
189
|
+
});
|
|
604
190
|
```
|
|
605
191
|
|
|
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).
|
|
192
|
+
#### JWS Session (signed, readable by client)
|
|
619
193
|
|
|
620
194
|
```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
|
-
});
|
|
195
|
+
import { useJWSSession, generateJWK } from "unjwt/adapters/h3v2";
|
|
631
196
|
|
|
632
|
-
|
|
633
|
-
visits: (data.visits ?? 0) + 1,
|
|
634
|
-
}));
|
|
197
|
+
const keys = await generateJWK("RS256"); // persist these!
|
|
635
198
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
key: keys,
|
|
639
|
-
maxAge:
|
|
199
|
+
app.get("/preferences", async (event) => {
|
|
200
|
+
const session = await useJWSSession(event, {
|
|
201
|
+
key: keys,
|
|
202
|
+
maxAge: "24h",
|
|
640
203
|
});
|
|
641
204
|
|
|
642
|
-
return {
|
|
643
|
-
encryptedSession: privateSession.data,
|
|
644
|
-
signedSession: publicSession.data,
|
|
645
|
-
};
|
|
205
|
+
return { theme: session.data.theme };
|
|
646
206
|
});
|
|
647
207
|
```
|
|
648
208
|
|
|
649
|
-
####
|
|
650
|
-
|
|
651
|
-
The `h3v2` adapter bundles session helpers that store data inside signed or encrypted JWTs.
|
|
209
|
+
#### Session features
|
|
652
210
|
|
|
653
|
-
-
|
|
654
|
-
-
|
|
211
|
+
- **Cookie chunking** — large tokens are automatically split across multiple cookies
|
|
212
|
+
- **Header-based tokens** — read from `Authorization: Bearer <token>` or custom headers via `sessionHeader`
|
|
213
|
+
- **Lifecycle hooks** — `onRead`, `onUpdate`, `onClear`, `onExpire`, `onError` for logging, refresh logic, etc.
|
|
214
|
+
- **Key lookup hooks** — `onUnsealKeyLookup` (JWE) / `onVerifyKeyLookup` (JWS) for key rotation
|
|
215
|
+
- **TypeScript generics** — strongly-typed session data via `useJWESession<MyData>(event, config)`
|
|
655
216
|
|
|
656
|
-
|
|
217
|
+
#### Refresh token pattern
|
|
657
218
|
|
|
658
219
|
```ts
|
|
659
|
-
import { H3, HTTPError, serve } from "h3v2";
|
|
660
|
-
|
|
661
220
|
import {
|
|
662
221
|
type SessionConfigJWE,
|
|
663
222
|
type SessionConfigJWS,
|
|
@@ -668,112 +227,31 @@ import {
|
|
|
668
227
|
generateJWK,
|
|
669
228
|
} from "unjwt/adapters/h3v2";
|
|
670
229
|
|
|
671
|
-
const
|
|
230
|
+
const atKeys = await generateJWK("RS256");
|
|
672
231
|
|
|
673
|
-
const
|
|
674
|
-
key:
|
|
232
|
+
const refreshConfig = {
|
|
233
|
+
key: process.env.REFRESH_SECRET!,
|
|
675
234
|
name: "refresh_token",
|
|
676
235
|
} satisfies SessionConfigJWE;
|
|
677
236
|
|
|
678
|
-
const
|
|
679
|
-
key:
|
|
237
|
+
const accessConfig = {
|
|
238
|
+
key: atKeys,
|
|
680
239
|
name: "access_token",
|
|
681
|
-
maxAge:
|
|
240
|
+
maxAge: "15m",
|
|
682
241
|
hooks: {
|
|
683
242
|
async onExpire({ event, config }) {
|
|
684
|
-
const
|
|
685
|
-
if (
|
|
686
|
-
|
|
687
|
-
|
|
243
|
+
const refresh = await getJWESession(event, refreshConfig);
|
|
244
|
+
if (refresh.data.sub) {
|
|
245
|
+
await updateJWSSession(event, config, {
|
|
246
|
+
sub: refresh.data.sub,
|
|
247
|
+
scope: refresh.data.scope,
|
|
248
|
+
});
|
|
688
249
|
}
|
|
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
250
|
},
|
|
698
251
|
},
|
|
699
252
|
} 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
253
|
```
|
|
763
254
|
|
|
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
255
|
## Development
|
|
778
256
|
|
|
779
257
|
<details>
|
|
@@ -784,7 +262,7 @@ serve(app);
|
|
|
784
262
|
- Install latest LTS version of [Node.js](https://nodejs.org/en/)
|
|
785
263
|
- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`
|
|
786
264
|
- Install dependencies using `pnpm install`
|
|
787
|
-
- Run
|
|
265
|
+
- Run tests using `pnpm test`
|
|
788
266
|
|
|
789
267
|
</details>
|
|
790
268
|
|