unjwt 0.8.3 → 0.8.5
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 +3 -9
- package/dist/_chunks/algorithms.mjs +1541 -0
- package/dist/_chunks/{_codec-jwe.d.mts → codec-jwe.d.mts} +40 -9
- package/dist/_chunks/{_codec-jws.mjs → codec-jws.mjs} +148 -95
- package/dist/_chunks/cookie.mjs +50 -0
- package/dist/_chunks/digest.mjs +10 -3
- package/dist/_chunks/error.d.mts +103 -0
- package/dist/_chunks/export.mjs +97 -0
- package/dist/_chunks/index.d.mts +5 -103
- package/dist/_chunks/index2.d.mts +206 -12
- package/dist/_chunks/index3.d.mts +12 -109
- package/dist/_chunks/{jwe.d.mts → index4.d.mts} +3 -1
- package/dist/_chunks/{jws.d.mts → index5.d.mts} +6 -2
- package/dist/_chunks/index6.d.mts +113 -0
- package/dist/_chunks/jwe.mjs +208 -74
- package/dist/_chunks/jwk.mjs +214 -2237
- package/dist/_chunks/jws.mjs +289 -202
- package/dist/_chunks/jwt.d.mts +2 -1
- package/dist/_chunks/{_jwt-internal.mjs → jwt.mjs} +124 -68
- package/dist/_chunks/libs/unsecure.d.mts +35 -25
- package/dist/_chunks/libs/unsecure.mjs +148 -302
- package/dist/_chunks/{_internal.d.mts → limits.d.mts} +1 -1
- package/dist/_chunks/payload.mjs +23 -0
- package/dist/_chunks/wrap.mjs +209 -0
- package/dist/adapters/elysia/index.d.mts +4 -4
- package/dist/adapters/elysia/index.mjs +5 -3
- package/dist/adapters/h3/index.d.mts +13 -13
- package/dist/adapters/h3/index.mjs +8 -10
- package/dist/adapters/h3v1/index.d.mts +5 -5
- package/dist/adapters/h3v1/index.mjs +6 -8
- package/dist/core/internal.mjs +1 -1
- package/dist/core/{jwe.d.mts → jwe/index.d.mts} +5 -4
- package/dist/core/jwe/index.mjs +5 -0
- package/dist/core/jwk/index.d.mts +6 -0
- package/dist/core/jwk/index.mjs +7 -0
- package/dist/core/{jws.d.mts → jws/index.d.mts} +5 -4
- package/dist/core/jws/index.mjs +5 -0
- package/dist/dpop/index.d.mts +73 -0
- package/dist/dpop/index.mjs +296 -0
- package/dist/error.d.mts +3 -0
- package/dist/error.mjs +20 -0
- package/dist/index.d.mts +10 -9
- package/dist/index.mjs +7 -3
- package/dist/session/index.d.mts +4 -4
- package/dist/session/index.mjs +6 -46
- package/dist/utils/index.d.mts +7 -0
- package/dist/{core/utils → utils}/index.mjs +4 -3
- package/package.json +22 -10
- package/dist/core/jwe.mjs +0 -5
- package/dist/core/jwk.d.mts +0 -207
- package/dist/core/jwk.mjs +0 -4
- package/dist/core/jws.mjs +0 -5
- package/dist/core/utils/index.d.mts +0 -6
- /package/dist/_chunks/{_cookie-attributes.d.mts → cookie-attributes.d.mts} +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import "./error.mjs";
|
|
1
2
|
import { ExpiresIn, JWEAsymmetricPrivateJWK, JWEAsymmetricPublicJWK, JWEDecryptJWK, JWEDecryptOptions, JWEEncryptJWK, JWEEncryptOptions, JWEHeaderParameters, JWKSet, JWK_HMAC, JWK_oct, JWSAsymmetricPrivateJWK, JWSAsymmetricPublicJWK, JWSHeaderParameters, JWSSignOptions, JWSVerifyJWK, JWSVerifyOptions, JWTClaims, StrictOmit } from "./index.mjs";
|
|
2
3
|
interface ExpirableCookieAttributes {
|
|
3
4
|
expires?: Date;
|
|
@@ -14,7 +15,7 @@ export type SessionUpdate<T extends SessionRecord = SessionClaims> = Partial<Ses
|
|
|
14
15
|
export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigMaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
|
|
15
16
|
readonly id: string | undefined;
|
|
16
17
|
readonly createdAt: number;
|
|
17
|
-
/** Expiry in ms
|
|
18
|
+
/** Expiry in ms: `exp`, or `iat + maxAge` when sooner; always a number when the session has a `maxAge`. */
|
|
18
19
|
readonly expiresAt: ConfigMaxAge extends ExpiresIn ? number : number | undefined;
|
|
19
20
|
readonly data: SessionData<T>;
|
|
20
21
|
readonly token: string | undefined;
|
|
@@ -28,7 +29,7 @@ export interface SessionManager<T extends SessionRecord = SessionClaims, ConfigM
|
|
|
28
29
|
}
|
|
29
30
|
/** The config members every session shares, whether it signs or seals its token. */
|
|
30
31
|
interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined> {
|
|
31
|
-
/** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens, drives cookie expiry; unset never expires. */
|
|
32
|
+
/** Sets `exp = iat + maxAge`, requires `exp` on incoming tokens and ends them at `iat + maxAge` at the latest, drives cookie expiry; unset never expires. */
|
|
32
33
|
maxAge?: MaxAge;
|
|
33
34
|
/**
|
|
34
35
|
* Reissue the session when it is read, needs `maxAge`.
|
|
@@ -49,6 +50,7 @@ interface SessionBaseConfig<MaxAge extends ExpiresIn | undefined = ExpiresIn | u
|
|
|
49
50
|
name?: string;
|
|
50
51
|
/**
|
|
51
52
|
* Header carrying a bare or `Bearer` token: a header name, `true` for `x-<name>-session` (cookie prefix stripped), or `false` for none.
|
|
53
|
+
* A request that carries it is read from it and never from the cookie, and writes no cookie back; any other value is a token failure (`onError`).
|
|
52
54
|
* Left unset, a session with cookies still reads `x-<name>-session`, and so does one with `cookie: false`.
|
|
53
55
|
*/
|
|
54
56
|
sessionHeader?: boolean | string;
|
|
@@ -68,7 +70,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
|
|
|
68
70
|
};
|
|
69
71
|
config: TConfig;
|
|
70
72
|
}) => void | Promise<void>;
|
|
71
|
-
/** Fires after a token is issued and its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
|
|
73
|
+
/** Fires after a token is issued and, on a cookie session, its cookie written, by `update()` or by `rolling`; a throw rejects without rollback. */
|
|
72
74
|
onUpdate?: (args: TArgs & {
|
|
73
75
|
session: TManager & {
|
|
74
76
|
id: string;
|
|
@@ -104,7 +106,7 @@ interface SessionHooks<TSession, TManager, TArgs extends Record<string, unknown>
|
|
|
104
106
|
}
|
|
105
107
|
/** Per-issue overrides for a single `update()` call. */
|
|
106
108
|
export interface SessionIssueOptions {
|
|
107
|
-
/** Lifetime for this issue only, overriding the config's `maxAge`. */
|
|
109
|
+
/** Lifetime for this issue only, overriding the config's `maxAge`; a read under that `maxAge` still ends the session at `iat + maxAge`. */
|
|
108
110
|
maxAge?: ExpiresIn;
|
|
109
111
|
}
|
|
110
112
|
/** In-request session state, mirroring the claims of the underlying token. */
|
|
@@ -113,7 +115,7 @@ export interface SessionState<T extends SessionRecord, MaxAge extends ExpiresIn
|
|
|
113
115
|
id: string | undefined;
|
|
114
116
|
/** In ms; mirrors the JWT `iat` claim (seconds). */
|
|
115
117
|
createdAt: number;
|
|
116
|
-
/** In ms; mirrors the JWT `exp` claim (seconds). */
|
|
118
|
+
/** In ms; mirrors the JWT `exp` claim (seconds), or `iat + maxAge` when sooner. */
|
|
117
119
|
expiresAt: MaxAge extends ExpiresIn ? number : number | undefined;
|
|
118
120
|
data: SessionData<T>;
|
|
119
121
|
token: string | undefined;
|
|
@@ -127,7 +129,7 @@ export type OpenedSessionState<T extends SessionRecord, MaxAge extends ExpiresIn
|
|
|
127
129
|
export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxAge extends ExpiresIn | undefined = ExpiresIn | undefined, TEvent = unknown> {
|
|
128
130
|
/** Reads the request's session once and returns its manager; later calls on the same request share that state. */
|
|
129
131
|
use: (event: TEvent) => Promise<SessionManager<T, MaxAge>>;
|
|
130
|
-
/** The token the request carries (
|
|
132
|
+
/** The session's token once `use()` read or wrote it, else the one the request carries (the session header before the cookie), unopened. */
|
|
131
133
|
token: (event: TEvent) => string | undefined;
|
|
132
134
|
/** Mints a token for `data` with a fresh `jti` and `iat`, and `exp` from `maxAge` (`options.maxAge` overrides it); reads and writes nothing. */
|
|
133
135
|
issue: (data?: Partial<SessionData<T>>, options?: SessionIssueOptions) => Promise<string>;
|
|
@@ -135,9 +137,13 @@ export interface SessionDefinition<T extends SessionRecord = SessionClaims, MaxA
|
|
|
135
137
|
open: (token: string) => Promise<OpenedSessionState<T, MaxAge>>;
|
|
136
138
|
}
|
|
137
139
|
interface SessionRequestBase {
|
|
138
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* The response being prepared; without it the session is read-only.
|
|
142
|
+
* With `errHeaders` too (h3 v2, which sends only those for a thrown handler), every cookie write lands in both.
|
|
143
|
+
*/
|
|
139
144
|
readonly res?: {
|
|
140
145
|
readonly headers: Headers;
|
|
146
|
+
readonly errHeaders?: Headers;
|
|
141
147
|
};
|
|
142
148
|
/** Per-request state shared with the framework, where `sessions` is kept; without it state is keyed on `req`, else on this object. */
|
|
143
149
|
readonly context?: Record<string, unknown>;
|
|
@@ -213,10 +219,35 @@ export interface JWSSessionHooks<TSession, TManager, TArgs extends Record<string
|
|
|
213
219
|
config: TConfig;
|
|
214
220
|
}) => JWKSet | JWSVerifyJWK | Promise<JWKSet | JWSVerifyJWK>;
|
|
215
221
|
}
|
|
216
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* A symmetric `oct` JWK, or a `CryptoKey` (requires pinned `alg` and `enc`): the one key seals and unseals.
|
|
224
|
+
*
|
|
225
|
+
* The `{ privateKey, publicKey? }` pair members are deprecated: a pair seals with its public half,
|
|
226
|
+
* so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or
|
|
227
|
+
* `defineJWSSession` for a key pair.
|
|
228
|
+
*/
|
|
217
229
|
type JWESessionKey = CryptoKey | JWEEncryptJWK | {
|
|
218
|
-
|
|
230
|
+
/**
|
|
231
|
+
* Unseals; a private JWK alone seals with its own public half.
|
|
232
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
233
|
+
*/
|
|
234
|
+
privateKey: JWEAsymmetricPrivateJWK;
|
|
235
|
+
/**
|
|
236
|
+
* Seals.
|
|
237
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
238
|
+
*/
|
|
219
239
|
publicKey?: JWEAsymmetricPublicJWK | CryptoKey;
|
|
240
|
+
} | {
|
|
241
|
+
/**
|
|
242
|
+
* Unseals; a private `CryptoKey` has no public half to derive, so it comes with one.
|
|
243
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
244
|
+
*/
|
|
245
|
+
privateKey: CryptoKey;
|
|
246
|
+
/**
|
|
247
|
+
* Seals.
|
|
248
|
+
* @deprecated A pair seals with its public half, so anyone holding the public key can mint a session the server accepts. Use an `oct` JWK, or `defineJWSSession` for a key pair.
|
|
249
|
+
*/
|
|
250
|
+
publicKey: JWEAsymmetricPublicJWK | CryptoKey;
|
|
220
251
|
};
|
|
221
252
|
/** JWE decrypt overrides, minus the options the adapter owns. */
|
|
222
253
|
export type SessionDecryptOptions = StrictOmit<JWEDecryptOptions, "algorithms" | "encryptionAlgorithms" | "typ" | "maxTokenAge" | "validateClaims" | "forceUint8Array" | "payload">;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { JWTError,
|
|
3
|
-
import {
|
|
1
|
+
import { safeJsonParse } from "./libs/unsecure.mjs";
|
|
2
|
+
import { JWTError, isJWTError } from "../error.mjs";
|
|
3
|
+
import { computeDurationInSeconds, expiredClaimMessage, isCryptoKey, isJWK, isJWKSet, isPlainObject, isPrivateJWK, isPublicJWK, isSymmetricJWK, quoteTokenValue } from "./jwt.mjs";
|
|
4
|
+
import { importKey } from "./algorithms.mjs";
|
|
4
5
|
import { sign, verify } from "./jws.mjs";
|
|
5
6
|
import { decrypt, encrypt } from "./jwe.mjs";
|
|
7
|
+
import { toPublicJWK } from "./export.mjs";
|
|
6
8
|
const RESERVED_CLAIMS = [
|
|
7
9
|
"jti",
|
|
8
10
|
"iat",
|
|
@@ -29,10 +31,12 @@ function readOnlyEventError(message) {
|
|
|
29
31
|
return new JWTError(message, "ERR_SESSION_READONLY_EVENT");
|
|
30
32
|
}
|
|
31
33
|
function expiredSessionError(options) {
|
|
32
|
-
const { id, expiresAt } = options;
|
|
34
|
+
const { id, createdAt, expiresAt } = options;
|
|
33
35
|
const exp = Math.floor(expiresAt / 1e3);
|
|
36
|
+
const iat = createdAt === void 0 ? void 0 : Math.floor(createdAt / 1e3);
|
|
34
37
|
return new JWTError(expiredClaimMessage(exp), "ERR_JWT_EXPIRED", {
|
|
35
38
|
jti: id,
|
|
39
|
+
iat,
|
|
36
40
|
exp
|
|
37
41
|
});
|
|
38
42
|
}
|
|
@@ -46,8 +50,8 @@ function rethrowAsInvalidToken(error, options) {
|
|
|
46
50
|
const TOKEN = /^[!#$%&'*+\-.^_`|~\w]+$/;
|
|
47
51
|
const CHUNK_SUFFIX = /\.\d+$/;
|
|
48
52
|
function assertSessionName(name, options) {
|
|
49
|
-
if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a cookie name (
|
|
50
|
-
if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name"
|
|
53
|
+
if (!isToken(name)) throw invalidConfigError(`${options.prefix} Session "name" must be a valid cookie name (got ${quoteTokenValue(name)}).`);
|
|
54
|
+
if (CHUNK_SUFFIX.test(name)) throw invalidConfigError(`${options.prefix} Session "name" ${quoteTokenValue(name)} cannot end in a dot and digits.`);
|
|
51
55
|
}
|
|
52
56
|
function isToken(value) {
|
|
53
57
|
return typeof value === "string" && TOKEN.test(value);
|
|
@@ -57,7 +61,7 @@ function assertCookiePrefix(name, attrs, options) {
|
|
|
57
61
|
const host = lower.startsWith("__host-");
|
|
58
62
|
if (!host && !lower.startsWith("__secure-")) return;
|
|
59
63
|
const rule = host ? "\"__Host-\" cookie must be Secure, with Path=/ and no Domain" : "\"__Secure-\" cookie must be Secure";
|
|
60
|
-
if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}
|
|
64
|
+
if (attrs.secure === false || host && (attrs.domain !== void 0 || attrs.path !== void 0 && attrs.path !== "/")) throw invalidConfigError(`${options.prefix} Session ${quoteTokenValue(name)}: a ${rule}.`);
|
|
61
65
|
}
|
|
62
66
|
function assertRolling(config, options) {
|
|
63
67
|
const { rolling } = config;
|
|
@@ -65,10 +69,10 @@ function assertRolling(config, options) {
|
|
|
65
69
|
if (config.maxAge === void 0) throw invalidConfigError(`${options.prefix} "rolling" needs a "maxAge" to extend.`);
|
|
66
70
|
if (rolling === true) return;
|
|
67
71
|
if (!isExpiresIn(rolling)) throw new JWTError(`${options.prefix} "rolling" must be true or a duration.`, "ERR_INVALID_OPTION");
|
|
68
|
-
if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge"
|
|
72
|
+
if (computeDurationInSeconds(rolling) >= computeDurationInSeconds(config.maxAge)) throw invalidConfigError(`${options.prefix} "rolling" must be shorter than "maxAge".`);
|
|
69
73
|
}
|
|
70
74
|
function assertKeyMaterial(key, options) {
|
|
71
|
-
if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key"
|
|
75
|
+
if (typeof key === "string") throw invalidKeyError(`${options.prefix} Session "key" cannot be a string, use deriveJWKFromPassword().`);
|
|
72
76
|
if (!key) throw invalidKeyError(`${options.prefix} Session "key" is required.`);
|
|
73
77
|
}
|
|
74
78
|
async function assertImportableKey(key, options) {
|
|
@@ -109,7 +113,7 @@ function sessionRequiredClaims(configured, options) {
|
|
|
109
113
|
return [...required];
|
|
110
114
|
}
|
|
111
115
|
function sanitizeSessionUpdate(update, options) {
|
|
112
|
-
if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object
|
|
116
|
+
if (!isPlainObject(update)) throw new JWTError(`${options.prefix} Session data must be a plain object.`, "ERR_INVALID_OPTION");
|
|
113
117
|
const sanitized = Object.create(null);
|
|
114
118
|
for (const key of Object.keys(update)) {
|
|
115
119
|
if (FORBIDDEN_KEYS.includes(key)) throw new JWTError(`${options.prefix} Session data cannot carry the key "${key}".`, "ERR_INVALID_OPTION");
|
|
@@ -131,12 +135,17 @@ function createSessionCodec(spec) {
|
|
|
131
135
|
const now = (config) => spec.issueOptions(config)?.currentDate?.getTime() ?? Date.now();
|
|
132
136
|
const openWith = async (config, token, key) => {
|
|
133
137
|
const payload = await spec.open(config, token, key).catch((error) => rethrowAsInvalidToken(error, { prefix }));
|
|
134
|
-
|
|
138
|
+
const clock = {
|
|
135
139
|
now: now(config),
|
|
136
|
-
clockTolerance: spec.openOptions(config)?.clockTolerance
|
|
140
|
+
clockTolerance: spec.openOptions(config)?.clockTolerance
|
|
141
|
+
};
|
|
142
|
+
assertNotFromTheFuture(payload, {
|
|
143
|
+
...clock,
|
|
137
144
|
prefix
|
|
138
145
|
});
|
|
139
|
-
|
|
146
|
+
const opened = openedSession(payload, { maxAge: config.maxAge });
|
|
147
|
+
assertNotPastMaxAge(opened, clock);
|
|
148
|
+
return opened;
|
|
140
149
|
};
|
|
141
150
|
const assertOpenKey = async (config) => {
|
|
142
151
|
if (spec.lookup(config, {})) return;
|
|
@@ -205,7 +214,7 @@ function assertKeyPair(pair, options) {
|
|
|
205
214
|
if (!options.isPublic(pair.publicKey)) throw invalidKeyError(`${prefix} Invalid ${family} key: "publicKey" must be ${options.publicHalf}.`, pair);
|
|
206
215
|
}
|
|
207
216
|
function assertPinnedCryptoKey(key, options) {
|
|
208
|
-
if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm
|
|
217
|
+
if (isCryptoKey(key) && !options.pinned) throw invalidConfigError(`${options.prefix} A CryptoKey session key carries no algorithm, set ${options.hint}.`);
|
|
209
218
|
}
|
|
210
219
|
function sessionPayload(state) {
|
|
211
220
|
const iat = Math.floor(state.createdAt / 1e3);
|
|
@@ -218,19 +227,33 @@ function sessionPayload(state) {
|
|
|
218
227
|
return payload;
|
|
219
228
|
}
|
|
220
229
|
function assertNotFromTheFuture(payload, options) {
|
|
221
|
-
|
|
222
|
-
|
|
230
|
+
if (payload.iat > Math.floor(options.now / 1e3) + toleranceSeconds(options.clockTolerance)) throw invalidTokenError(`${options.prefix} Invalid session token: "iat" is in the future (${(/* @__PURE__ */ new Date(payload.iat * 1e3)).toISOString()}).`);
|
|
231
|
+
}
|
|
232
|
+
function assertNotPastMaxAge(opened, options) {
|
|
233
|
+
const { expiresAt } = opened;
|
|
234
|
+
if (expiresAt === void 0) return;
|
|
235
|
+
if (expiresAt <= options.now - toleranceSeconds(options.clockTolerance) * 1e3) throw expiredSessionError({
|
|
236
|
+
id: opened.id,
|
|
237
|
+
createdAt: opened.createdAt,
|
|
238
|
+
expiresAt
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function toleranceSeconds(clockTolerance) {
|
|
242
|
+
return clockTolerance === void 0 ? 0 : computeDurationInSeconds(clockTolerance);
|
|
223
243
|
}
|
|
224
|
-
function openedSession(payload) {
|
|
244
|
+
function openedSession(payload, options) {
|
|
225
245
|
const data = Object.create(null);
|
|
226
246
|
for (const key of Object.keys(payload)) {
|
|
227
247
|
if (RESERVED_CLAIMS.includes(key)) continue;
|
|
228
248
|
data[key] = payload[key];
|
|
229
249
|
}
|
|
250
|
+
const createdAt = payload.iat * 1e3;
|
|
251
|
+
const exp = payload.exp ? payload.exp * 1e3 : void 0;
|
|
252
|
+
const bound = resolveExpiresAt(createdAt, options);
|
|
230
253
|
return {
|
|
231
254
|
id: payload.jti,
|
|
232
|
-
createdAt
|
|
233
|
-
expiresAt:
|
|
255
|
+
createdAt,
|
|
256
|
+
expiresAt: exp === void 0 || bound === void 0 ? exp ?? bound : Math.min(exp, bound),
|
|
234
257
|
data
|
|
235
258
|
};
|
|
236
259
|
}
|
|
@@ -290,11 +313,8 @@ function getEncryptKey(config, prefix) {
|
|
|
290
313
|
assertKeyMaterial(key, { prefix });
|
|
291
314
|
let resolved;
|
|
292
315
|
if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
|
|
293
|
-
else if (isKeyPair(key))
|
|
294
|
-
|
|
295
|
-
resolved = key.publicKey;
|
|
296
|
-
}
|
|
297
|
-
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK or a CryptoKey.`, key);
|
|
316
|
+
else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).publicKey;
|
|
317
|
+
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK or a CryptoKey.`, key);
|
|
298
318
|
assertPinnedAlgEnc(resolved, config, prefix);
|
|
299
319
|
return resolved;
|
|
300
320
|
}
|
|
@@ -303,21 +323,27 @@ function getDecryptKey(config, prefix) {
|
|
|
303
323
|
assertKeyMaterial(key, { prefix });
|
|
304
324
|
let resolved;
|
|
305
325
|
if (isCryptoKey(key) || isSymmetricJWK(key)) resolved = key;
|
|
306
|
-
else if (isKeyPair(key))
|
|
307
|
-
|
|
308
|
-
resolved = key.privateKey;
|
|
309
|
-
}
|
|
310
|
-
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key. It must be a JWK, a private JWK, or a CryptoKey.`, key);
|
|
326
|
+
else if (isKeyPair(key)) resolved = jweKeyPair(key, prefix).privateKey;
|
|
327
|
+
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWE key, expected a JWK, a private JWK or a CryptoKey.`, key);
|
|
311
328
|
assertPinnedAlgEnc(resolved, config, prefix);
|
|
312
329
|
return resolved;
|
|
313
330
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
331
|
+
function jweKeyPair(key, prefix) {
|
|
332
|
+
const pair = {
|
|
333
|
+
privateKey: key.privateKey,
|
|
334
|
+
publicKey: key.publicKey ?? derivedPublicHalf(key.privateKey, prefix)
|
|
335
|
+
};
|
|
336
|
+
assertKeyPair(pair, {
|
|
316
337
|
prefix,
|
|
317
338
|
family: "JWE",
|
|
318
339
|
isPublic: (half) => isCryptoKey(half) || isPublicJWK(half),
|
|
319
340
|
publicHalf: "a public JWK or a CryptoKey"
|
|
320
341
|
});
|
|
342
|
+
return pair;
|
|
343
|
+
}
|
|
344
|
+
function derivedPublicHalf(privateKey, prefix) {
|
|
345
|
+
if (isPrivateJWK(privateKey)) return toPublicJWK(privateKey);
|
|
346
|
+
throw invalidKeyError(`${prefix} Invalid JWE key: a CryptoKey "privateKey" needs its "publicKey".`, privateKey);
|
|
321
347
|
}
|
|
322
348
|
function assertPinnedAlgEnc(key, config, prefix) {
|
|
323
349
|
const { alg, enc } = config.jwe?.encryptOptions ?? {};
|
|
@@ -327,6 +353,43 @@ function assertPinnedAlgEnc(key, config, prefix) {
|
|
|
327
353
|
hint: "\"jwe.encryptOptions.alg\" and \"enc\""
|
|
328
354
|
});
|
|
329
355
|
}
|
|
356
|
+
const kSessionPending = Symbol("unjwt_session_pending");
|
|
357
|
+
function sessionInstant(source) {
|
|
358
|
+
const now = source.codec.now(source.config);
|
|
359
|
+
return now - now % 1e3;
|
|
360
|
+
}
|
|
361
|
+
function mintSessionId(config) {
|
|
362
|
+
return config.generateId?.() || crypto.randomUUID();
|
|
363
|
+
}
|
|
364
|
+
function emptySession(binding) {
|
|
365
|
+
const createdAt = sessionInstant(binding);
|
|
366
|
+
return typedSession({
|
|
367
|
+
id: void 0,
|
|
368
|
+
createdAt,
|
|
369
|
+
expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
|
|
370
|
+
data: Object.create(null),
|
|
371
|
+
token: void 0
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
function storedSession(store) {
|
|
375
|
+
const state = store.get();
|
|
376
|
+
return state === void 0 ? void 0 : typedSession(state);
|
|
377
|
+
}
|
|
378
|
+
function blankSession() {
|
|
379
|
+
return typedSession({
|
|
380
|
+
expiresAt: void 0,
|
|
381
|
+
data: {}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
function openSessionToken(codec, config, options) {
|
|
385
|
+
return codec.open(config, options.token, options.args).then(typedOpened);
|
|
386
|
+
}
|
|
387
|
+
function typedSession(state) {
|
|
388
|
+
return state;
|
|
389
|
+
}
|
|
390
|
+
function typedOpened(opened) {
|
|
391
|
+
return opened;
|
|
392
|
+
}
|
|
330
393
|
const CHUNK_MARKER = "__chunked__";
|
|
331
394
|
const DEFAULT_CHUNK_MAX_LENGTH = 4e3;
|
|
332
395
|
function readChunked(store, name) {
|
|
@@ -374,6 +437,13 @@ function setCookieValues(setCookies) {
|
|
|
374
437
|
function setCookieName(setCookie) {
|
|
375
438
|
return setCookiePair(setCookie)?.name;
|
|
376
439
|
}
|
|
440
|
+
function mirrorSetCookie(name, headers) {
|
|
441
|
+
const { from, to } = headers;
|
|
442
|
+
const isThisCookie = (line) => setCookieName(line) === name;
|
|
443
|
+
const lines = [...to.getSetCookie().filter((line) => !isThisCookie(line)), ...from.getSetCookie().filter((line) => isThisCookie(line))];
|
|
444
|
+
to.delete("set-cookie");
|
|
445
|
+
for (const line of lines) to.append("set-cookie", line);
|
|
446
|
+
}
|
|
377
447
|
function chunkName(name, index) {
|
|
378
448
|
return `${name}.${index}`;
|
|
379
449
|
}
|
|
@@ -412,25 +482,25 @@ function decodeValue(value) {
|
|
|
412
482
|
function resolveTransport(driver, event, options) {
|
|
413
483
|
const handed = driver.handed?.(event);
|
|
414
484
|
if (handed) return {
|
|
485
|
+
source: "handed",
|
|
415
486
|
read: () => handed.token,
|
|
416
487
|
readonly: false,
|
|
417
488
|
writable: () => true
|
|
418
489
|
};
|
|
419
490
|
const { config, name } = options;
|
|
420
|
-
const header =
|
|
421
|
-
if (config.cookie === false) return {
|
|
422
|
-
|
|
491
|
+
const header = presentedHeader(driver, event, options);
|
|
492
|
+
if (header || config.cookie === false) return {
|
|
493
|
+
source: "header",
|
|
494
|
+
read: config.sessionHeader === false ? void 0 : () => header?.token,
|
|
495
|
+
malformed: header?.malformed,
|
|
423
496
|
readonly: false,
|
|
424
497
|
writable: () => true
|
|
425
498
|
};
|
|
426
499
|
const jar = driver.cookies(event);
|
|
427
500
|
const store = responseStore(jar);
|
|
428
501
|
const transport = {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
if (written === "") return void 0;
|
|
432
|
-
return (written ?? readChunked(jar, name)) || header?.() || void 0;
|
|
433
|
-
},
|
|
502
|
+
source: "cookie",
|
|
503
|
+
read: () => cookieToken(jar, name),
|
|
434
504
|
readonly: !store,
|
|
435
505
|
writable: () => driver.writable?.(event) ?? true
|
|
436
506
|
};
|
|
@@ -448,7 +518,7 @@ function resolveTransport(driver, event, options) {
|
|
|
448
518
|
return transport;
|
|
449
519
|
}
|
|
450
520
|
function assertTokenSource(transport, options) {
|
|
451
|
-
if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source
|
|
521
|
+
if (!transport.read) throw invalidConfigError(`${options.prefix} Session has no token source, enable "cookie" or "sessionHeader".`);
|
|
452
522
|
}
|
|
453
523
|
function assertWritable(transport, options) {
|
|
454
524
|
const { operation, prefix } = options;
|
|
@@ -458,7 +528,7 @@ function assertWritable(transport, options) {
|
|
|
458
528
|
function assertSessionHeader(header, options) {
|
|
459
529
|
if (header === void 0 || typeof header === "boolean") return;
|
|
460
530
|
if (isToken(header)) return;
|
|
461
|
-
throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name
|
|
531
|
+
throw invalidConfigError(`${options.prefix} Session "sessionHeader" must be true, false or a header name (got ${quoteTokenValue(header)}).`);
|
|
462
532
|
}
|
|
463
533
|
function sessionHeaderName(sessionName, configured) {
|
|
464
534
|
if (typeof configured === "string") return configured.toLowerCase();
|
|
@@ -471,11 +541,23 @@ function headerToken(value) {
|
|
|
471
541
|
if (/^bearer$/i.test(first)) return words.length === 2 ? second : void 0;
|
|
472
542
|
return words.length === 1 && first ? first : void 0;
|
|
473
543
|
}
|
|
474
|
-
function
|
|
544
|
+
function presentedHeader(driver, event, options) {
|
|
475
545
|
const { config, name } = options;
|
|
476
546
|
if (config.sessionHeader === false) return void 0;
|
|
477
547
|
const header = sessionHeaderName(name, config.sessionHeader);
|
|
478
|
-
|
|
548
|
+
const value = driver.header(event, header);
|
|
549
|
+
if (typeof value !== "string") return void 0;
|
|
550
|
+
const token = headerToken(value);
|
|
551
|
+
if (token) return { token };
|
|
552
|
+
return {
|
|
553
|
+
token: void 0,
|
|
554
|
+
malformed: invalidTokenError(`${driver.prefix} Invalid session token: the "${header}" header holds neither a token nor "Bearer <token>".`)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function cookieToken(jar, name) {
|
|
558
|
+
const written = jar.written ? readChunked({ read: (key) => jar.written?.(key) }, name) : void 0;
|
|
559
|
+
if (written === "") return void 0;
|
|
560
|
+
return (written ?? readChunked(jar, name)) || void 0;
|
|
479
561
|
}
|
|
480
562
|
function responseStore(jar) {
|
|
481
563
|
const write = jar.write?.bind(jar);
|
|
@@ -521,7 +603,8 @@ function bindSession(event, config, options) {
|
|
|
521
603
|
function sessionToken(binding) {
|
|
522
604
|
const { transport } = binding;
|
|
523
605
|
assertTokenSource(transport, { prefix: binding.driver.prefix });
|
|
524
|
-
|
|
606
|
+
const session = storedSession(binding.store);
|
|
607
|
+
return session ? session.token : transport.read();
|
|
525
608
|
}
|
|
526
609
|
function recordSlotStore(record, slot) {
|
|
527
610
|
return {
|
|
@@ -561,44 +644,7 @@ function claimSessionName(record, options) {
|
|
|
561
644
|
return;
|
|
562
645
|
}
|
|
563
646
|
if (held === family) return;
|
|
564
|
-
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request
|
|
565
|
-
}
|
|
566
|
-
const kSessionPending = Symbol("unjwt_session_pending");
|
|
567
|
-
function sessionInstant(source) {
|
|
568
|
-
const now = source.codec.now(source.config);
|
|
569
|
-
return now - now % 1e3;
|
|
570
|
-
}
|
|
571
|
-
function mintSessionId(config) {
|
|
572
|
-
return config.generateId?.() || crypto.randomUUID();
|
|
573
|
-
}
|
|
574
|
-
function emptySession(binding) {
|
|
575
|
-
const createdAt = sessionInstant(binding);
|
|
576
|
-
return typedSession({
|
|
577
|
-
id: void 0,
|
|
578
|
-
createdAt,
|
|
579
|
-
expiresAt: resolveExpiresAt(createdAt, { maxAge: binding.config.maxAge }),
|
|
580
|
-
data: Object.create(null),
|
|
581
|
-
token: void 0
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
function storedSession(store) {
|
|
585
|
-
const state = store.get();
|
|
586
|
-
return state === void 0 ? void 0 : typedSession(state);
|
|
587
|
-
}
|
|
588
|
-
function blankSession() {
|
|
589
|
-
return typedSession({
|
|
590
|
-
expiresAt: void 0,
|
|
591
|
-
data: {}
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
function openSessionToken(codec, config, options) {
|
|
595
|
-
return codec.open(config, options.token, options.args).then(typedOpened);
|
|
596
|
-
}
|
|
597
|
-
function typedSession(state) {
|
|
598
|
-
return state;
|
|
599
|
-
}
|
|
600
|
-
function typedOpened(opened) {
|
|
601
|
-
return opened;
|
|
647
|
+
throw invalidConfigError(`${prefix} Session name "${name}" is already used by a ${held.toUpperCase()} session on this request.`);
|
|
602
648
|
}
|
|
603
649
|
async function loadSession(binding) {
|
|
604
650
|
const { config, codec, store, args, transport } = binding;
|
|
@@ -621,6 +667,7 @@ async function loadSession(binding) {
|
|
|
621
667
|
},
|
|
622
668
|
error: expiredSessionError({
|
|
623
669
|
id: session.id,
|
|
670
|
+
createdAt: session.createdAt,
|
|
624
671
|
expiresAt: session.expiresAt
|
|
625
672
|
})
|
|
626
673
|
});
|
|
@@ -630,6 +677,10 @@ async function loadSession(binding) {
|
|
|
630
677
|
}
|
|
631
678
|
const session = emptySession(binding);
|
|
632
679
|
store.set(session);
|
|
680
|
+
if (transport.malformed) {
|
|
681
|
+
await fireError(binding, transport.malformed);
|
|
682
|
+
return session;
|
|
683
|
+
}
|
|
633
684
|
const token = transport.read();
|
|
634
685
|
if (!token) return session;
|
|
635
686
|
session.token = token;
|
|
@@ -660,12 +711,7 @@ async function loadSession(binding) {
|
|
|
660
711
|
});
|
|
661
712
|
} else {
|
|
662
713
|
if (isJWTError(failure.error, "ERR_SESSION_TOKEN_INVALID") && isTokenFailure(failure.error)) dropSessionCookie(binding);
|
|
663
|
-
await
|
|
664
|
-
session: binding.actions.manager(binding),
|
|
665
|
-
...args,
|
|
666
|
-
error: failure.error,
|
|
667
|
-
config
|
|
668
|
-
});
|
|
714
|
+
await fireError(binding, failure.error);
|
|
669
715
|
}
|
|
670
716
|
return session;
|
|
671
717
|
}
|
|
@@ -692,6 +738,14 @@ async function rollSession(binding, session) {
|
|
|
692
738
|
if (transport.readonly || !transport.writable()) return;
|
|
693
739
|
await binding.actions.roll(binding);
|
|
694
740
|
}
|
|
741
|
+
async function fireError(binding, error) {
|
|
742
|
+
await binding.config.hooks?.onError?.({
|
|
743
|
+
session: binding.actions.manager(binding),
|
|
744
|
+
...binding.args,
|
|
745
|
+
error,
|
|
746
|
+
config: binding.config
|
|
747
|
+
});
|
|
748
|
+
}
|
|
695
749
|
async function fireExpire(binding, expired) {
|
|
696
750
|
await binding.config.hooks?.onExpire?.({
|
|
697
751
|
session: expired.session,
|
|
@@ -797,8 +851,7 @@ function sessionManager(binding) {
|
|
|
797
851
|
return (state() ?? blankSession()).data;
|
|
798
852
|
},
|
|
799
853
|
get token() {
|
|
800
|
-
|
|
801
|
-
return session ? session.token : sessionToken(binding);
|
|
854
|
+
return sessionToken(binding);
|
|
802
855
|
},
|
|
803
856
|
update: async (update, options) => {
|
|
804
857
|
await issueSession(binding, update, options);
|
|
@@ -822,7 +875,7 @@ function sessionActions() {
|
|
|
822
875
|
function snapshotSession(session) {
|
|
823
876
|
return {
|
|
824
877
|
...session,
|
|
825
|
-
data:
|
|
878
|
+
data: safeJsonParse(JSON.stringify(session.data))
|
|
826
879
|
};
|
|
827
880
|
}
|
|
828
881
|
function defineSession(config, options) {
|
|
@@ -914,7 +967,7 @@ function getSignKey(config, prefix) {
|
|
|
914
967
|
assertJWSKeyPair(key, prefix);
|
|
915
968
|
resolved = key.privateKey;
|
|
916
969
|
}
|
|
917
|
-
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key
|
|
970
|
+
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a private JWK or a CryptoKey.`, key);
|
|
918
971
|
assertPinnedAlg(resolved, config, prefix);
|
|
919
972
|
return resolved;
|
|
920
973
|
}
|
|
@@ -927,7 +980,7 @@ function getVerifyKey(config, prefix) {
|
|
|
927
980
|
assertJWSKeyPair(key, prefix);
|
|
928
981
|
resolved = Array.isArray(key.publicKey) ? { keys: key.publicKey } : key.publicKey;
|
|
929
982
|
}
|
|
930
|
-
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key
|
|
983
|
+
if (!resolved) throw invalidKeyError(`${prefix} Invalid JWS key, expected a symmetric JWK, a public JWK/set or a CryptoKey.`, key);
|
|
931
984
|
assertPinnedAlg(resolved, config, prefix);
|
|
932
985
|
return resolved;
|
|
933
986
|
}
|
|
@@ -936,7 +989,7 @@ function assertJWSKeyPair(key, prefix) {
|
|
|
936
989
|
prefix,
|
|
937
990
|
family: "JWS",
|
|
938
991
|
isPublic: (half) => isCryptoKey(half) || isPublicJWK(half) || isPublicKeyList(Array.isArray(half) ? half : isJWKSet(half) ? half.keys : void 0),
|
|
939
|
-
publicHalf: "a public JWK, a non-empty
|
|
992
|
+
publicHalf: "a public JWK, a non-empty JWK set or a CryptoKey"
|
|
940
993
|
});
|
|
941
994
|
}
|
|
942
995
|
function isPublicKeyList(keys) {
|
|
@@ -949,4 +1002,4 @@ function assertPinnedAlg(key, config, prefix) {
|
|
|
949
1002
|
hint: "\"jws.signOptions.alg\""
|
|
950
1003
|
});
|
|
951
1004
|
}
|
|
952
|
-
export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, openSessionToken, sessionActions, setCookieName, setCookieValues };
|
|
1005
|
+
export { bindSession, createJWECodec, createJWSCodec, defineSession, dropSession, encodeSession, invalidConfigError, issueSession, loadSession, mirrorSetCookie, openSessionToken, sessionActions, setCookieName, setCookieValues };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mirrorSetCookie, setCookieName, setCookieValues } from "./codec-jws.mjs";
|
|
2
|
+
import { parse, parseSetCookie, serialize } from "./libs/cookie-es.mjs";
|
|
3
|
+
function webCookieJar(request, response) {
|
|
4
|
+
let cookies;
|
|
5
|
+
const jar = { read: (name) => (cookies ||= requestCookies(request))[name] };
|
|
6
|
+
if (response) {
|
|
7
|
+
const { headers, errHeaders } = response;
|
|
8
|
+
jar.written = (name) => setCookieValues(headers.getSetCookie())[name];
|
|
9
|
+
jar.write = (name, value, attrs) => {
|
|
10
|
+
setCookie(headers, {
|
|
11
|
+
name,
|
|
12
|
+
value,
|
|
13
|
+
...attrs
|
|
14
|
+
});
|
|
15
|
+
if (errHeaders) mirrorSetCookie(name, {
|
|
16
|
+
from: headers,
|
|
17
|
+
to: errHeaders
|
|
18
|
+
});
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
return jar;
|
|
22
|
+
}
|
|
23
|
+
function requestCookies(request) {
|
|
24
|
+
const header = request.get("cookie");
|
|
25
|
+
return header ? parse(header) : Object.create(null);
|
|
26
|
+
}
|
|
27
|
+
function setCookie(headers, cookie) {
|
|
28
|
+
const line = serialize({
|
|
29
|
+
path: "/",
|
|
30
|
+
...cookie
|
|
31
|
+
});
|
|
32
|
+
const key = cookieKey(cookie);
|
|
33
|
+
const kept = headers.getSetCookie().filter((existing) => {
|
|
34
|
+
const parsed = parseSetCookie(existing);
|
|
35
|
+
return parsed ? cookieKey(parsed) !== key : setCookieName(existing) !== cookie.name;
|
|
36
|
+
});
|
|
37
|
+
headers.delete("set-cookie");
|
|
38
|
+
for (const existing of kept) headers.append("set-cookie", existing);
|
|
39
|
+
headers.append("set-cookie", line);
|
|
40
|
+
}
|
|
41
|
+
function cookieKey(cookie) {
|
|
42
|
+
const domain = (cookie.domain || "").replace(/^\./, "").toLowerCase();
|
|
43
|
+
return [
|
|
44
|
+
cookie.name,
|
|
45
|
+
domain,
|
|
46
|
+
cookie.path || "/",
|
|
47
|
+
cookie.partitioned ? "1" : "0"
|
|
48
|
+
].join(";");
|
|
49
|
+
}
|
|
50
|
+
export { requestCookies, webCookieJar };
|