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
package/dist/_chunks/jws.mjs
CHANGED
|
@@ -1,9 +1,94 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { JWTError
|
|
3
|
-
import {
|
|
4
|
-
import { JWS_ALG_CTX,
|
|
1
|
+
import { textEncoder } from "./libs/unsecure.mjs";
|
|
2
|
+
import { JWTError } from "../error.mjs";
|
|
3
|
+
import { applyTypDefault, base64UrlStringify, computeJwtTimeClaims, getPlaintextBytes, isJWK, isJWKSet, parsePayloadBytes, parsePayloadSegment, parsePayloadText, validateCriticalHeadersJWS } from "./jwt.mjs";
|
|
4
|
+
import { JWS_ALG_CTX, asJWTError, asKeyLookupError, assertCompactToken, assertConsumerOptions, assertEntryLimit, assertJWKSetCandidates, assertOctKeyLength, assertOptionEntry, assertProducerConfirmation, assertProducerCrit, assertProducerKeyHeaders, assertProtectedOnlyOption, assertSerializationObject, buildJWKSetFilter, checkAlgAllowed, checkAlgSupported, checkProtectedTyp, checkSerializationEntry, copyJsonObject, decodeProtectedHeader, decodeSegment, getJWKsFromSet, importKey, keyMatchesHeader, resolveKeyInput, resolveLimit, serializeProtectedHeader, sign as sign$1, stringKeyError, verify as verify$1 } from "./algorithms.mjs";
|
|
5
|
+
import { inferJWSAllowedAlgorithms, isJWSAlgorithm, jwsHashOf, modulusLengthOf } from "./digest.mjs";
|
|
6
|
+
import { resolvePayloadMode, validateClaimsIfJson } from "./payload.mjs";
|
|
7
|
+
async function resolveSigningKey(alg, key, usage) {
|
|
8
|
+
if (key instanceof Uint8Array) {
|
|
9
|
+
if (alg !== "HS256" && alg !== "HS384" && alg !== "HS512") throw new JWTError(`${alg} needs an asymmetric key, not raw bytes.`, "ERR_JWK_INVALID");
|
|
10
|
+
assertOctKeyLength(key, alg);
|
|
11
|
+
return crypto.subtle.importKey("raw", key, {
|
|
12
|
+
name: "HMAC",
|
|
13
|
+
hash: jwsHashOf(alg)
|
|
14
|
+
}, false, [usage]);
|
|
15
|
+
}
|
|
16
|
+
if (RSA_SIGNATURE_ALGORITHMS.has(alg)) {
|
|
17
|
+
const modulusLength = modulusLengthOf(key.algorithm);
|
|
18
|
+
if (typeof modulusLength !== "number" || modulusLength < 2048) throw new JWTError(`${alg} requires a key modulusLength of at least 2048 bits`, "ERR_JWK_INVALID");
|
|
19
|
+
}
|
|
20
|
+
return key;
|
|
21
|
+
}
|
|
22
|
+
function assertNotStringKey(key) {
|
|
23
|
+
if (typeof key === "string") throw stringKeyError();
|
|
24
|
+
}
|
|
25
|
+
function asKeyUsageError(error, alg) {
|
|
26
|
+
return asJWTError(error, {
|
|
27
|
+
code: "ERR_JWK_INVALID",
|
|
28
|
+
message: `Key is not usable for ${alg}.`
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const RSA_SIGNATURE_ALGORITHMS = /* @__PURE__ */ new Set([
|
|
32
|
+
"RS256",
|
|
33
|
+
"RS384",
|
|
34
|
+
"RS512",
|
|
35
|
+
"PS256",
|
|
36
|
+
"PS384",
|
|
37
|
+
"PS512"
|
|
38
|
+
]);
|
|
39
|
+
function assertJWSAlgSupported(alg) {
|
|
40
|
+
const error = checkJWSAlgSupported(alg);
|
|
41
|
+
if (error) throw error;
|
|
42
|
+
}
|
|
43
|
+
function checkJWSAlgSupported(alg) {
|
|
44
|
+
return checkAlgSupported(alg, {
|
|
45
|
+
supported: isJWSAlgorithm,
|
|
46
|
+
code: "ERR_JWS_ALG_NOT_ALLOWED",
|
|
47
|
+
label: "Algorithm",
|
|
48
|
+
role: "signing algorithm"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function checkJWSB64(header) {
|
|
52
|
+
if (Object.hasOwn(header, "b64") && typeof header.b64 !== "boolean") return new JWTError("Invalid JWS: \"b64\" must be a boolean.", "ERR_JWS_INVALID");
|
|
53
|
+
if (header.b64 !== false) return void 0;
|
|
54
|
+
if (Array.isArray(header.crit) && header.crit.includes("b64")) return void 0;
|
|
55
|
+
return new JWTError("Invalid JWS: \"b64\" must be listed in \"crit\".", "ERR_JWS_INVALID");
|
|
56
|
+
}
|
|
57
|
+
function assertB64Option(header) {
|
|
58
|
+
if (!header || !Object.hasOwn(header, "b64") || typeof header.b64 === "boolean") return;
|
|
59
|
+
throw new JWTError("\"b64\" must be a boolean.", "ERR_INVALID_OPTION");
|
|
60
|
+
}
|
|
61
|
+
function applyB64Header(header) {
|
|
62
|
+
assertB64Option(header);
|
|
63
|
+
if (header.b64 === true) {
|
|
64
|
+
if (!(Array.isArray(header.crit) && header.crit.includes("b64"))) delete header.b64;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (header.b64 !== false) return;
|
|
68
|
+
const crit = header.crit;
|
|
69
|
+
if (crit === void 0) header.crit = ["b64"];
|
|
70
|
+
else if (Array.isArray(crit) && !crit.includes("b64")) header.crit = [...crit, "b64"];
|
|
71
|
+
}
|
|
72
|
+
function decodeJWSPayload(payloadSegment, useB64, mode) {
|
|
73
|
+
if (!useB64) return mode === "string" ? payloadSegment : parsePayloadBytes(textEncoder.encode(payloadSegment), {
|
|
74
|
+
payload: mode,
|
|
75
|
+
family: "JWS"
|
|
76
|
+
});
|
|
77
|
+
try {
|
|
78
|
+
return parsePayloadSegment(payloadSegment, {
|
|
79
|
+
payload: mode,
|
|
80
|
+
family: "JWS"
|
|
81
|
+
});
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof JWTError) throw error;
|
|
84
|
+
throw new JWTError("Invalid JWS: Payload could not be decoded.", "ERR_JWS_INVALID", error);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
5
87
|
function buildJWSProtectedHeader(alg, key, userHeader, options) {
|
|
6
|
-
const safeHeader =
|
|
88
|
+
const safeHeader = copyJsonObject(userHeader, {
|
|
89
|
+
code: "ERR_INVALID_OPTION",
|
|
90
|
+
message: "\"protectedHeader\" must hold only JSON values."
|
|
91
|
+
});
|
|
7
92
|
const header = {};
|
|
8
93
|
if (isJWK(key) && key.kid) header.kid = key.kid;
|
|
9
94
|
if (safeHeader) Object.assign(header, safeHeader);
|
|
@@ -54,189 +139,6 @@ async function verifyWithKeyMaterial(alg, rawKeyMaterial, options) {
|
|
|
54
139
|
}), "verify");
|
|
55
140
|
return verify$1(alg, verificationKey, signature, signingInput);
|
|
56
141
|
}
|
|
57
|
-
async function signMulti(payload, signers, options = {}) {
|
|
58
|
-
if (!Array.isArray(signers) || signers.length === 0) throw new JWTError("signMulti requires at least one signer.", "ERR_JWS_INVALID");
|
|
59
|
-
for (const [index, signer] of signers.entries()) {
|
|
60
|
-
assertOptionEntry(signer, {
|
|
61
|
-
label: `signers[${index}]`,
|
|
62
|
-
objects: ["protectedHeader", "unprotectedHeader"]
|
|
63
|
-
});
|
|
64
|
-
assertNotStringKey(signer.key);
|
|
65
|
-
assertProtectedOnlyOption(signer.unprotectedHeader, {
|
|
66
|
-
tier: "unprotected",
|
|
67
|
-
family: "JWS"
|
|
68
|
-
});
|
|
69
|
-
assertProducerKeyHeaders(signer.protectedHeader, {
|
|
70
|
-
tier: "protected",
|
|
71
|
-
family: "JWS"
|
|
72
|
-
});
|
|
73
|
-
assertProducerKeyHeaders(signer.unprotectedHeader, {
|
|
74
|
-
tier: "unprotected",
|
|
75
|
-
family: "JWS"
|
|
76
|
-
});
|
|
77
|
-
assertB64Option(signer.protectedHeader);
|
|
78
|
-
}
|
|
79
|
-
assertProducerConfirmation(payload);
|
|
80
|
-
const resolvedAlgs = signers.map((s, i) => _resolveSignerAlg(s, i));
|
|
81
|
-
const b64 = _resolveB64(signers);
|
|
82
|
-
const computedPayload = computeJwtTimeClaims(payload, options);
|
|
83
|
-
const payloadBytes = getPlaintextBytes(computedPayload || payload);
|
|
84
|
-
const payloadEncoded = b64 ? base64UrlStringify(payloadBytes) : parsePayloadText(payloadBytes);
|
|
85
|
-
const wireSignatures = [];
|
|
86
|
-
for (let i = 0; i < signers.length; i++) {
|
|
87
|
-
const signer = signers[i];
|
|
88
|
-
const alg = resolvedAlgs[i];
|
|
89
|
-
const protectedHeader = buildJWSProtectedHeader(alg, signer.key, signer.protectedHeader, {
|
|
90
|
-
payload,
|
|
91
|
-
b64
|
|
92
|
-
});
|
|
93
|
-
const unprotectedHeader = sanitizeObjectCopy(signer.unprotectedHeader);
|
|
94
|
-
_assertDisjointHeaders(protectedHeader, unprotectedHeader);
|
|
95
|
-
const protectedHeaderEncoded = base64UrlStringify(serializeProtectedHeader(protectedHeader));
|
|
96
|
-
const signingInputBytes = textEncoder.encode(`${protectedHeaderEncoded}.${payloadEncoded}`);
|
|
97
|
-
let signatureBytes;
|
|
98
|
-
try {
|
|
99
|
-
const signingKey = await resolveSigningKey(alg, await importKey(signer.key, {
|
|
100
|
-
alg,
|
|
101
|
-
expect: "private"
|
|
102
|
-
}), "sign");
|
|
103
|
-
signatureBytes = await sign$1(alg, signingKey, signingInputBytes);
|
|
104
|
-
} catch (error) {
|
|
105
|
-
throw asKeyUsageError(error, alg);
|
|
106
|
-
}
|
|
107
|
-
const wireSig = {
|
|
108
|
-
protected: protectedHeaderEncoded,
|
|
109
|
-
signature: base64UrlStringify(signatureBytes)
|
|
110
|
-
};
|
|
111
|
-
if (unprotectedHeader && Object.keys(unprotectedHeader).length > 0) wireSig.header = unprotectedHeader;
|
|
112
|
-
wireSignatures.push(wireSig);
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
payload: payloadEncoded,
|
|
116
|
-
signatures: wireSignatures
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
async function verifyMulti(jws, keyOrLookup, options = {}) {
|
|
120
|
-
assertConsumerOptions(options);
|
|
121
|
-
const payloadMode = resolvePayloadMode(options);
|
|
122
|
-
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
123
|
-
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
124
|
-
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
125
|
-
let lastError;
|
|
126
|
-
let cryptoAttempted = false;
|
|
127
|
-
for (let i = 0; i < general.signatures.length; i++) {
|
|
128
|
-
const read = _readSignatureHeaders(general.signatures[i], i);
|
|
129
|
-
if (read.error) {
|
|
130
|
-
lastError = read.error;
|
|
131
|
-
continue;
|
|
132
|
-
}
|
|
133
|
-
const { wireSig, protectedHeader, unprotectedHeader, effective, alg } = read;
|
|
134
|
-
const typError = checkProtectedTyp(options.typ, protectedHeader, {
|
|
135
|
-
family: "JWS",
|
|
136
|
-
label: `signature[${i}]`
|
|
137
|
-
});
|
|
138
|
-
if (typError) {
|
|
139
|
-
lastError = typError;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
let rawKeyMaterial;
|
|
143
|
-
try {
|
|
144
|
-
rawKeyMaterial = await resolveKeyInput(keyOrLookup, {
|
|
145
|
-
header: effective,
|
|
146
|
-
token: wireSig.protected ?? "",
|
|
147
|
-
what: `signature[${i}]`
|
|
148
|
-
});
|
|
149
|
-
} catch (err) {
|
|
150
|
-
lastError = asKeyLookupError(err, `signature[${i}]`);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (typeof rawKeyMaterial === "string") {
|
|
154
|
-
lastError = stringKeyError();
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const algError = checkAlgAllowed(alg, rawKeyMaterial, {
|
|
158
|
-
ctx: JWS_ALG_CTX,
|
|
159
|
-
algorithms: options.algorithms,
|
|
160
|
-
allowSymmetric: options.allowSymmetricJWKSet
|
|
161
|
-
});
|
|
162
|
-
if (algError) {
|
|
163
|
-
lastError = algError;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (options.strictSignerMatch && !keyMatchesHeader(effective, rawKeyMaterial, {
|
|
167
|
-
ctx: JWS_ALG_CTX,
|
|
168
|
-
allowSymmetric: options.allowSymmetricJWKSet
|
|
169
|
-
})) continue;
|
|
170
|
-
let signatureBytes;
|
|
171
|
-
try {
|
|
172
|
-
signatureBytes = decodeSegment(wireSig.signature, {
|
|
173
|
-
family: "JWS",
|
|
174
|
-
what: "Signature"
|
|
175
|
-
});
|
|
176
|
-
} catch (err) {
|
|
177
|
-
lastError = asJWTError(err, INVALID_JWS);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const signingInputBytes = textEncoder.encode(`${wireSig.protected ?? ""}.${general.payload}`);
|
|
181
|
-
cryptoAttempted = true;
|
|
182
|
-
try {
|
|
183
|
-
if (!await verifyWithKey(alg, rawKeyMaterial, {
|
|
184
|
-
signature: signatureBytes,
|
|
185
|
-
signingInput: signingInputBytes,
|
|
186
|
-
kid: effective.kid,
|
|
187
|
-
allowSymmetric: options.allowSymmetricJWKSet,
|
|
188
|
-
keyCandidates
|
|
189
|
-
})) {
|
|
190
|
-
lastError = new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
194
|
-
const useB64 = protectedHeader.b64 !== false;
|
|
195
|
-
const payload = decodeJWSPayload(general.payload, useB64, payloadMode);
|
|
196
|
-
validateClaimsIfJson(payload, payloadMode, options);
|
|
197
|
-
const result = {
|
|
198
|
-
payload,
|
|
199
|
-
protectedHeader,
|
|
200
|
-
signerIndex: i
|
|
201
|
-
};
|
|
202
|
-
if (unprotectedHeader) result.signerHeader = unprotectedHeader;
|
|
203
|
-
return result;
|
|
204
|
-
} catch (err) {
|
|
205
|
-
const error = asJWTError(err, INVALID_SIGNATURE);
|
|
206
|
-
if (!PER_SIGNATURE_ERROR_CODES.has(error.code)) throw error;
|
|
207
|
-
lastError = error;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
if (options.strictSignerMatch && !cryptoAttempted) throw new JWTError("No signature matched the provided key under strictSignerMatch.", "ERR_JWS_NO_MATCHING_SIGNER");
|
|
211
|
-
throw lastError ?? new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
212
|
-
}
|
|
213
|
-
async function verifyMultiAll(jws, keyResolver, options = {}) {
|
|
214
|
-
assertConsumerOptions(options);
|
|
215
|
-
const payloadMode = resolvePayloadMode(options);
|
|
216
|
-
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
217
|
-
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
218
|
-
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
219
|
-
const outcomes = [];
|
|
220
|
-
for (let i = 0; i < general.signatures.length; i++) outcomes.push(await _verifyOneSignature(general.payload, general.signatures[i], i, keyResolver, {
|
|
221
|
-
options,
|
|
222
|
-
payloadMode,
|
|
223
|
-
keyCandidates
|
|
224
|
-
}));
|
|
225
|
-
return outcomes;
|
|
226
|
-
}
|
|
227
|
-
function generalToFlattenedJWS(jws) {
|
|
228
|
-
assertSerializationObject(jws, "JWS");
|
|
229
|
-
if (!Array.isArray(jws.signatures)) throw new JWTError("Invalid JWS JSON Serialization: missing \"signatures\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
230
|
-
if (jws.signatures.length !== 1) throw new JWTError(`Flattened JWS Serialization requires exactly one signature, got ${jws.signatures.length}.`, "ERR_JWS_INVALID_SERIALIZATION");
|
|
231
|
-
const [signature] = jws.signatures;
|
|
232
|
-
const flattened = {
|
|
233
|
-
payload: jws.payload,
|
|
234
|
-
signature: signature.signature
|
|
235
|
-
};
|
|
236
|
-
if (signature.protected !== void 0) flattened.protected = signature.protected;
|
|
237
|
-
if (signature.header !== void 0) flattened.header = signature.header;
|
|
238
|
-
return flattened;
|
|
239
|
-
}
|
|
240
142
|
const INVALID_JWS = {
|
|
241
143
|
code: "ERR_JWS_INVALID",
|
|
242
144
|
message: "Invalid JWS."
|
|
@@ -256,8 +158,8 @@ const PER_SIGNATURE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
256
158
|
]);
|
|
257
159
|
function _checkUnprotectedHeader(header) {
|
|
258
160
|
if (!header) return void 0;
|
|
259
|
-
if (Object.hasOwn(header, "crit")) return new JWTError("Invalid JWS: \"crit\" must be integrity protected
|
|
260
|
-
if (Object.hasOwn(header, "b64")) return new JWTError("Invalid JWS: \"b64\" must
|
|
161
|
+
if (Object.hasOwn(header, "crit")) return new JWTError("Invalid JWS: \"crit\" must be integrity protected.", "ERR_JWS_INVALID");
|
|
162
|
+
if (Object.hasOwn(header, "b64")) return new JWTError("Invalid JWS: \"b64\" must be in the protected header.", "ERR_JWS_INVALID");
|
|
261
163
|
}
|
|
262
164
|
function _readSignatureHeaders(entry, index) {
|
|
263
165
|
const shapeError = checkSerializationEntry(entry, {
|
|
@@ -270,12 +172,13 @@ function _readSignatureHeaders(entry, index) {
|
|
|
270
172
|
const wireSig = entry;
|
|
271
173
|
if (typeof wireSig.signature !== "string") return { error: new JWTError(`Signature[${index}] is missing the "signature" field.`, "ERR_JWS_INVALID") };
|
|
272
174
|
let protectedHeader;
|
|
175
|
+
let unprotectedHeader;
|
|
273
176
|
try {
|
|
274
177
|
protectedHeader = decodeProtectedHeader(wireSig.protected, "JWS");
|
|
178
|
+
unprotectedHeader = copyJsonObject(wireSig.header, INVALID_JWS);
|
|
275
179
|
} catch (err) {
|
|
276
180
|
return { error: asJWTError(err, INVALID_JWS) };
|
|
277
181
|
}
|
|
278
|
-
const unprotectedHeader = sanitizeObjectCopy(wireSig.header);
|
|
279
182
|
const failed = (error) => ({
|
|
280
183
|
error,
|
|
281
184
|
protectedHeader,
|
|
@@ -290,7 +193,7 @@ function _readSignatureHeaders(entry, index) {
|
|
|
290
193
|
}
|
|
291
194
|
const effective = _mergeHeaders(protectedHeader, unprotectedHeader);
|
|
292
195
|
const alg = effective.alg;
|
|
293
|
-
if (!alg) return failed(new JWTError(`Invalid JWS: Signature[${index}] has no "alg" in its
|
|
196
|
+
if (!alg) return failed(new JWTError(`Invalid JWS: Signature[${index}] has no "alg" in its headers.`, "ERR_JWS_INVALID"));
|
|
294
197
|
const algError = checkJWSAlgSupported(alg) ?? checkJWSB64(protectedHeader);
|
|
295
198
|
if (algError) return failed(algError);
|
|
296
199
|
return {
|
|
@@ -323,13 +226,13 @@ function _resolveB64(signers) {
|
|
|
323
226
|
const raw = signers[i].protectedHeader?.b64;
|
|
324
227
|
if (raw === void 0) continue;
|
|
325
228
|
if (decided === void 0) decided = raw;
|
|
326
|
-
else if (decided !== raw) throw new JWTError(
|
|
229
|
+
else if (decided !== raw) throw new JWTError("Inconsistent \"b64\" across signers.", "ERR_JWS_B64_INCONSISTENT");
|
|
327
230
|
}
|
|
328
231
|
return decided ?? true;
|
|
329
232
|
}
|
|
330
233
|
function _assertDisjointHeaders(protectedHeader, unprotectedHeader) {
|
|
331
234
|
if (!protectedHeader || !unprotectedHeader) return;
|
|
332
|
-
for (const k of Object.keys(unprotectedHeader)) if (k in protectedHeader) throw new JWTError(`JWS header parameter "${k}"
|
|
235
|
+
for (const k of Object.keys(unprotectedHeader)) if (k in protectedHeader) throw new JWTError(`Invalid JWS: header parameter "${k}" is set in more than one header.`, "ERR_JWS_HEADER_PARAMS_NOT_DISJOINT");
|
|
333
236
|
}
|
|
334
237
|
function _mergeHeaders(protectedHeader, unprotectedHeader) {
|
|
335
238
|
if (!unprotectedHeader) return protectedHeader;
|
|
@@ -350,7 +253,7 @@ function _readGeneralSerialization(input, signatureLimit) {
|
|
|
350
253
|
function _normalizeSerialization(input) {
|
|
351
254
|
assertSerializationObject(input, "JWS");
|
|
352
255
|
if ("signatures" in input && Array.isArray(input.signatures)) {
|
|
353
|
-
if ("signature" in input || "protected" in input) throw new JWTError("Invalid JWS JSON Serialization: \"signatures\"
|
|
256
|
+
if ("signature" in input || "protected" in input) throw new JWTError("Invalid JWS JSON Serialization: \"signatures\" excludes top-level \"signature\" and \"protected\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
354
257
|
return input;
|
|
355
258
|
}
|
|
356
259
|
if ("signature" in input) {
|
|
@@ -363,7 +266,7 @@ function _normalizeSerialization(input) {
|
|
|
363
266
|
signatures: [signature]
|
|
364
267
|
};
|
|
365
268
|
}
|
|
366
|
-
throw new JWTError("Invalid JWS JSON Serialization: missing
|
|
269
|
+
throw new JWTError("Invalid JWS JSON Serialization: missing \"signatures\" or \"signature\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
367
270
|
}
|
|
368
271
|
async function _verifyOneSignature(payload, entry, index, keyResolver, context) {
|
|
369
272
|
const { options, payloadMode, keyCandidates } = context;
|
|
@@ -432,8 +335,7 @@ async function _verifyOneSignature(payload, entry, index, keyResolver, context)
|
|
|
432
335
|
}
|
|
433
336
|
let decodedPayload;
|
|
434
337
|
try {
|
|
435
|
-
|
|
436
|
-
decodedPayload = decodeJWSPayload(payload, useB64, payloadMode);
|
|
338
|
+
decodedPayload = decodeJWSPayload(payload, protectedHeader.b64 !== false, payloadMode);
|
|
437
339
|
} catch (err) {
|
|
438
340
|
return fail(asJWTError(err, INVALID_JWS), protectedHeader, unprotectedHeader);
|
|
439
341
|
}
|
|
@@ -451,6 +353,192 @@ async function _verifyOneSignature(payload, entry, index, keyResolver, context)
|
|
|
451
353
|
if (unprotectedHeader) outcome.signerHeader = unprotectedHeader;
|
|
452
354
|
return outcome;
|
|
453
355
|
}
|
|
356
|
+
async function signMulti(payload, signers, options = {}) {
|
|
357
|
+
if (!Array.isArray(signers) || signers.length === 0) throw new JWTError("signMulti requires at least one signer.", "ERR_JWS_INVALID");
|
|
358
|
+
for (const [index, signer] of signers.entries()) {
|
|
359
|
+
assertOptionEntry(signer, {
|
|
360
|
+
label: `signers[${index}]`,
|
|
361
|
+
objects: ["protectedHeader", "unprotectedHeader"]
|
|
362
|
+
});
|
|
363
|
+
assertNotStringKey(signer.key);
|
|
364
|
+
assertProtectedOnlyOption(signer.unprotectedHeader, {
|
|
365
|
+
tier: "unprotected",
|
|
366
|
+
family: "JWS"
|
|
367
|
+
});
|
|
368
|
+
assertProducerKeyHeaders(signer.protectedHeader, {
|
|
369
|
+
tier: "protected",
|
|
370
|
+
family: "JWS"
|
|
371
|
+
});
|
|
372
|
+
assertProducerKeyHeaders(signer.unprotectedHeader, {
|
|
373
|
+
tier: "unprotected",
|
|
374
|
+
family: "JWS"
|
|
375
|
+
});
|
|
376
|
+
assertB64Option(signer.protectedHeader);
|
|
377
|
+
}
|
|
378
|
+
assertProducerConfirmation(payload);
|
|
379
|
+
const resolvedAlgs = signers.map((s, i) => _resolveSignerAlg(s, i));
|
|
380
|
+
const b64 = _resolveB64(signers);
|
|
381
|
+
const computedPayload = computeJwtTimeClaims(payload, options);
|
|
382
|
+
const payloadBytes = getPlaintextBytes(computedPayload || payload);
|
|
383
|
+
const payloadEncoded = b64 ? base64UrlStringify(payloadBytes) : parsePayloadText(payloadBytes);
|
|
384
|
+
const wireSignatures = [];
|
|
385
|
+
for (let i = 0; i < signers.length; i++) {
|
|
386
|
+
const signer = signers[i];
|
|
387
|
+
const alg = resolvedAlgs[i];
|
|
388
|
+
const protectedHeader = buildJWSProtectedHeader(alg, signer.key, signer.protectedHeader, {
|
|
389
|
+
payload,
|
|
390
|
+
b64
|
|
391
|
+
});
|
|
392
|
+
const unprotectedHeader = copyJsonObject(signer.unprotectedHeader, {
|
|
393
|
+
code: "ERR_INVALID_OPTION",
|
|
394
|
+
message: "\"unprotectedHeader\" must hold only JSON values."
|
|
395
|
+
});
|
|
396
|
+
_assertDisjointHeaders(protectedHeader, unprotectedHeader);
|
|
397
|
+
const protectedHeaderEncoded = base64UrlStringify(serializeProtectedHeader(protectedHeader));
|
|
398
|
+
const signingInputBytes = textEncoder.encode(`${protectedHeaderEncoded}.${payloadEncoded}`);
|
|
399
|
+
let signatureBytes;
|
|
400
|
+
try {
|
|
401
|
+
const signingKey = await resolveSigningKey(alg, await importKey(signer.key, {
|
|
402
|
+
alg,
|
|
403
|
+
expect: "private"
|
|
404
|
+
}), "sign");
|
|
405
|
+
signatureBytes = await sign$1(alg, signingKey, signingInputBytes);
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw asKeyUsageError(error, alg);
|
|
408
|
+
}
|
|
409
|
+
const wireSig = {
|
|
410
|
+
protected: protectedHeaderEncoded,
|
|
411
|
+
signature: base64UrlStringify(signatureBytes)
|
|
412
|
+
};
|
|
413
|
+
if (unprotectedHeader && Object.keys(unprotectedHeader).length > 0) wireSig.header = unprotectedHeader;
|
|
414
|
+
wireSignatures.push(wireSig);
|
|
415
|
+
}
|
|
416
|
+
return {
|
|
417
|
+
payload: payloadEncoded,
|
|
418
|
+
signatures: wireSignatures
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async function verifyMulti(jws, keyOrLookup, options = {}) {
|
|
422
|
+
assertConsumerOptions(options);
|
|
423
|
+
const payloadMode = resolvePayloadMode(options);
|
|
424
|
+
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
425
|
+
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
426
|
+
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
427
|
+
let lastError;
|
|
428
|
+
let cryptoAttempted = false;
|
|
429
|
+
for (let i = 0; i < general.signatures.length; i++) {
|
|
430
|
+
const read = _readSignatureHeaders(general.signatures[i], i);
|
|
431
|
+
if (read.error) {
|
|
432
|
+
lastError = read.error;
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const { wireSig, protectedHeader, unprotectedHeader, effective, alg } = read;
|
|
436
|
+
const typError = checkProtectedTyp(options.typ, protectedHeader, {
|
|
437
|
+
family: "JWS",
|
|
438
|
+
label: `signature[${i}]`
|
|
439
|
+
});
|
|
440
|
+
if (typError) {
|
|
441
|
+
lastError = typError;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
let rawKeyMaterial;
|
|
445
|
+
try {
|
|
446
|
+
rawKeyMaterial = await resolveKeyInput(keyOrLookup, {
|
|
447
|
+
header: effective,
|
|
448
|
+
token: wireSig.protected ?? "",
|
|
449
|
+
what: `signature[${i}]`
|
|
450
|
+
});
|
|
451
|
+
} catch (err) {
|
|
452
|
+
lastError = asKeyLookupError(err, `signature[${i}]`);
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
if (typeof rawKeyMaterial === "string") {
|
|
456
|
+
lastError = stringKeyError();
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const algError = checkAlgAllowed(alg, rawKeyMaterial, {
|
|
460
|
+
ctx: JWS_ALG_CTX,
|
|
461
|
+
algorithms: options.algorithms,
|
|
462
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
463
|
+
});
|
|
464
|
+
if (algError) {
|
|
465
|
+
lastError = algError;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (options.strictSignerMatch && !keyMatchesHeader(effective, rawKeyMaterial, {
|
|
469
|
+
ctx: JWS_ALG_CTX,
|
|
470
|
+
allowSymmetric: options.allowSymmetricJWKSet
|
|
471
|
+
})) continue;
|
|
472
|
+
let signatureBytes;
|
|
473
|
+
try {
|
|
474
|
+
signatureBytes = decodeSegment(wireSig.signature, {
|
|
475
|
+
family: "JWS",
|
|
476
|
+
what: "Signature"
|
|
477
|
+
});
|
|
478
|
+
} catch (err) {
|
|
479
|
+
lastError = asJWTError(err, INVALID_JWS);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const signingInputBytes = textEncoder.encode(`${wireSig.protected ?? ""}.${general.payload}`);
|
|
483
|
+
cryptoAttempted = true;
|
|
484
|
+
try {
|
|
485
|
+
if (!await verifyWithKey(alg, rawKeyMaterial, {
|
|
486
|
+
signature: signatureBytes,
|
|
487
|
+
signingInput: signingInputBytes,
|
|
488
|
+
kid: effective.kid,
|
|
489
|
+
allowSymmetric: options.allowSymmetricJWKSet,
|
|
490
|
+
keyCandidates
|
|
491
|
+
})) {
|
|
492
|
+
lastError = new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
496
|
+
const useB64 = protectedHeader.b64 !== false;
|
|
497
|
+
const payload = decodeJWSPayload(general.payload, useB64, payloadMode);
|
|
498
|
+
validateClaimsIfJson(payload, payloadMode, options);
|
|
499
|
+
const result = {
|
|
500
|
+
payload,
|
|
501
|
+
protectedHeader,
|
|
502
|
+
signerIndex: i
|
|
503
|
+
};
|
|
504
|
+
if (unprotectedHeader) result.signerHeader = unprotectedHeader;
|
|
505
|
+
return result;
|
|
506
|
+
} catch (err) {
|
|
507
|
+
const error = asJWTError(err, INVALID_SIGNATURE);
|
|
508
|
+
if (!PER_SIGNATURE_ERROR_CODES.has(error.code)) throw error;
|
|
509
|
+
lastError = error;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (options.strictSignerMatch && !cryptoAttempted) throw new JWTError("No signature matched the provided key under strictSignerMatch.", "ERR_JWS_NO_MATCHING_SIGNER");
|
|
513
|
+
throw lastError ?? new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
514
|
+
}
|
|
515
|
+
async function verifyMultiAll(jws, keyResolver, options = {}) {
|
|
516
|
+
assertConsumerOptions(options);
|
|
517
|
+
const payloadMode = resolvePayloadMode(options);
|
|
518
|
+
const signatureLimit = resolveLimit(options.limits, "signatures");
|
|
519
|
+
const keyCandidates = resolveLimit(options.limits, "keyCandidates");
|
|
520
|
+
const general = _readGeneralSerialization(jws, signatureLimit);
|
|
521
|
+
const outcomes = [];
|
|
522
|
+
for (let i = 0; i < general.signatures.length; i++) outcomes.push(await _verifyOneSignature(general.payload, general.signatures[i], i, keyResolver, {
|
|
523
|
+
options,
|
|
524
|
+
payloadMode,
|
|
525
|
+
keyCandidates
|
|
526
|
+
}));
|
|
527
|
+
return outcomes;
|
|
528
|
+
}
|
|
529
|
+
function generalToFlattenedJWS(jws) {
|
|
530
|
+
assertSerializationObject(jws, "JWS");
|
|
531
|
+
if (!Array.isArray(jws.signatures)) throw new JWTError("Invalid JWS JSON Serialization: missing \"signatures\".", "ERR_JWS_INVALID_SERIALIZATION");
|
|
532
|
+
if (jws.signatures.length !== 1) throw new JWTError(`Flattened JWS Serialization requires exactly one signature, got ${jws.signatures.length}.`, "ERR_JWS_INVALID_SERIALIZATION");
|
|
533
|
+
const [signature] = jws.signatures;
|
|
534
|
+
const flattened = {
|
|
535
|
+
payload: jws.payload,
|
|
536
|
+
signature: signature.signature
|
|
537
|
+
};
|
|
538
|
+
if (signature.protected !== void 0) flattened.protected = signature.protected;
|
|
539
|
+
if (signature.header !== void 0) flattened.header = signature.header;
|
|
540
|
+
return flattened;
|
|
541
|
+
}
|
|
454
542
|
async function sign(payload, key, options = {}) {
|
|
455
543
|
assertOptionEntry(options, {
|
|
456
544
|
label: "options",
|
|
@@ -466,7 +554,7 @@ async function sign(payload, key, options = {}) {
|
|
|
466
554
|
let { alg } = options;
|
|
467
555
|
if (!alg) {
|
|
468
556
|
if (isJWK(key) && key.alg) alg = key.alg;
|
|
469
|
-
else throw new JWTError("JWS \"alg\"
|
|
557
|
+
else throw new JWTError("JWS \"alg\" must be provided in options", "ERR_JWS_ALG_MISSING");
|
|
470
558
|
}
|
|
471
559
|
assertJWSAlgSupported(alg);
|
|
472
560
|
let signingKey;
|
|
@@ -488,7 +576,7 @@ async function sign(payload, key, options = {}) {
|
|
|
488
576
|
if (useB64) payloadEncoded = base64UrlStringify(payloadBytes);
|
|
489
577
|
else {
|
|
490
578
|
payloadEncoded = parsePayloadText(payloadBytes);
|
|
491
|
-
if (payloadEncoded.includes(".")) throw new JWTError("Invalid JWS: unencoded compact payload must not contain \".\"
|
|
579
|
+
if (payloadEncoded.includes(".")) throw new JWTError("Invalid JWS: unencoded compact payload must not contain \".\".", "ERR_JWS_INVALID");
|
|
492
580
|
}
|
|
493
581
|
const signingInputString = `${protectedHeaderEncoded}.${payloadEncoded}`;
|
|
494
582
|
const signingInputBytes = textEncoder.encode(signingInputString);
|
|
@@ -538,8 +626,7 @@ async function verify(jws, key, options = {}) {
|
|
|
538
626
|
keyCandidates: keyCandidateLimit
|
|
539
627
|
})) throw new JWTError("JWS signature verification failed.", "ERR_JWS_SIGNATURE_INVALID");
|
|
540
628
|
validateCriticalHeadersJWS(protectedHeader, options.recognizedHeaders);
|
|
541
|
-
const
|
|
542
|
-
const payload = decodeJWSPayload(payloadEncoded, useB64, payloadMode);
|
|
629
|
+
const payload = decodeJWSPayload(payloadEncoded, protectedHeader.b64 !== false, payloadMode);
|
|
543
630
|
validateClaimsIfJson(payload, payloadMode, options);
|
|
544
631
|
return {
|
|
545
632
|
payload,
|
package/dist/_chunks/jwt.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JWTErrorCode } from "./error.mjs";
|
|
2
|
+
import { Duration, JOSEPayload, JOSEPayloadMode, JWTClaimValidationOptions, JWTClaims, JWTTimeClaimOptions, PayloadOf } from "./index.mjs";
|
|
2
3
|
/** Set `typ: "JWT"` on a header without `typ` when the payload is an object (not a `Uint8Array`). */
|
|
3
4
|
export declare function applyTypDefault<T extends {
|
|
4
5
|
typ?: string;
|