398 lines
13 KiB
JavaScript
398 lines
13 KiB
JavaScript
import {
|
|
TokenVerificationError,
|
|
TokenVerificationErrorAction,
|
|
TokenVerificationErrorReason
|
|
} from "./chunk-RZ7A7F6X.mjs";
|
|
|
|
// src/runtime.ts
|
|
import { webcrypto as crypto } from "#crypto";
|
|
var globalFetch = fetch.bind(globalThis);
|
|
var runtime = {
|
|
crypto,
|
|
get fetch() {
|
|
return process.env.NODE_ENV === "test" ? fetch : globalFetch;
|
|
},
|
|
AbortController: globalThis.AbortController,
|
|
Blob: globalThis.Blob,
|
|
FormData: globalThis.FormData,
|
|
Headers: globalThis.Headers,
|
|
Request: globalThis.Request,
|
|
Response: globalThis.Response
|
|
};
|
|
|
|
// src/util/rfc4648.ts
|
|
var base64url = {
|
|
parse(string, opts) {
|
|
return parse(string, base64UrlEncoding, opts);
|
|
},
|
|
stringify(data, opts) {
|
|
return stringify(data, base64UrlEncoding, opts);
|
|
}
|
|
};
|
|
var base64UrlEncoding = {
|
|
chars: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_",
|
|
bits: 6
|
|
};
|
|
function parse(string, encoding, opts = {}) {
|
|
if (!encoding.codes) {
|
|
encoding.codes = {};
|
|
for (let i = 0; i < encoding.chars.length; ++i) {
|
|
encoding.codes[encoding.chars[i]] = i;
|
|
}
|
|
}
|
|
if (!opts.loose && string.length * encoding.bits & 7) {
|
|
throw new SyntaxError("Invalid padding");
|
|
}
|
|
let end = string.length;
|
|
while (string[end - 1] === "=") {
|
|
--end;
|
|
if (!opts.loose && !((string.length - end) * encoding.bits & 7)) {
|
|
throw new SyntaxError("Invalid padding");
|
|
}
|
|
}
|
|
const out = new (opts.out ?? Uint8Array)(end * encoding.bits / 8 | 0);
|
|
let bits = 0;
|
|
let buffer = 0;
|
|
let written = 0;
|
|
for (let i = 0; i < end; ++i) {
|
|
const value = encoding.codes[string[i]];
|
|
if (value === void 0) {
|
|
throw new SyntaxError("Invalid character " + string[i]);
|
|
}
|
|
buffer = buffer << encoding.bits | value;
|
|
bits += encoding.bits;
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
out[written++] = 255 & buffer >> bits;
|
|
}
|
|
}
|
|
if (bits >= encoding.bits || 255 & buffer << 8 - bits) {
|
|
throw new SyntaxError("Unexpected end of data");
|
|
}
|
|
return out;
|
|
}
|
|
function stringify(data, encoding, opts = {}) {
|
|
const { pad = true } = opts;
|
|
const mask = (1 << encoding.bits) - 1;
|
|
let out = "";
|
|
let bits = 0;
|
|
let buffer = 0;
|
|
for (let i = 0; i < data.length; ++i) {
|
|
buffer = buffer << 8 | 255 & data[i];
|
|
bits += 8;
|
|
while (bits > encoding.bits) {
|
|
bits -= encoding.bits;
|
|
out += encoding.chars[mask & buffer >> bits];
|
|
}
|
|
}
|
|
if (bits) {
|
|
out += encoding.chars[mask & buffer << encoding.bits - bits];
|
|
}
|
|
if (pad) {
|
|
while (out.length * encoding.bits & 7) {
|
|
out += "=";
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// src/jwt/algorithms.ts
|
|
var algToHash = {
|
|
RS256: "SHA-256",
|
|
RS384: "SHA-384",
|
|
RS512: "SHA-512"
|
|
};
|
|
var RSA_ALGORITHM_NAME = "RSASSA-PKCS1-v1_5";
|
|
var jwksAlgToCryptoAlg = {
|
|
RS256: RSA_ALGORITHM_NAME,
|
|
RS384: RSA_ALGORITHM_NAME,
|
|
RS512: RSA_ALGORITHM_NAME
|
|
};
|
|
var algs = Object.keys(algToHash);
|
|
function getCryptoAlgorithm(algorithmName) {
|
|
const hash = algToHash[algorithmName];
|
|
const name = jwksAlgToCryptoAlg[algorithmName];
|
|
if (!hash || !name) {
|
|
throw new Error(`Unsupported algorithm ${algorithmName}, expected one of ${algs.join(",")}.`);
|
|
}
|
|
return {
|
|
hash: { name: algToHash[algorithmName] },
|
|
name: jwksAlgToCryptoAlg[algorithmName]
|
|
};
|
|
}
|
|
|
|
// src/jwt/assertions.ts
|
|
var isArrayString = (s) => {
|
|
return Array.isArray(s) && s.length > 0 && s.every((a) => typeof a === "string");
|
|
};
|
|
var assertAudienceClaim = (aud, audience) => {
|
|
const audienceList = [audience].flat().filter((a) => !!a);
|
|
const audList = [aud].flat().filter((a) => !!a);
|
|
const shouldVerifyAudience = audienceList.length > 0 && audList.length > 0;
|
|
if (!shouldVerifyAudience) {
|
|
return;
|
|
}
|
|
if (typeof aud === "string") {
|
|
if (!audienceList.includes(aud)) {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Invalid JWT audience claim (aud) ${JSON.stringify(aud)}. Is not included in "${JSON.stringify(
|
|
audienceList
|
|
)}".`
|
|
});
|
|
}
|
|
} else if (isArrayString(aud)) {
|
|
if (!aud.some((a) => audienceList.includes(a))) {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Invalid JWT audience claim array (aud) ${JSON.stringify(aud)}. Is not included in "${JSON.stringify(
|
|
audienceList
|
|
)}".`
|
|
});
|
|
}
|
|
}
|
|
};
|
|
var assertHeaderType = (typ, allowedTypes = "JWT") => {
|
|
if (typeof typ === "undefined") {
|
|
return;
|
|
}
|
|
const allowed = Array.isArray(allowedTypes) ? allowedTypes : [allowedTypes];
|
|
if (!allowed.includes(typ)) {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
message: `Invalid JWT type ${JSON.stringify(typ)}. Expected "${allowed.join(", ")}".`
|
|
});
|
|
}
|
|
};
|
|
var assertHeaderAlgorithm = (alg) => {
|
|
if (!algs.includes(alg)) {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenInvalidAlgorithm,
|
|
message: `Invalid JWT algorithm ${JSON.stringify(alg)}. Supported: ${algs}.`
|
|
});
|
|
}
|
|
};
|
|
var assertSubClaim = (sub) => {
|
|
if (typeof sub !== "string") {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Subject claim (sub) is required and must be a string. Received ${JSON.stringify(sub)}.`
|
|
});
|
|
}
|
|
};
|
|
var assertAuthorizedPartiesClaim = (azp, authorizedParties) => {
|
|
if (!azp || !authorizedParties || authorizedParties.length === 0) {
|
|
return;
|
|
}
|
|
if (!authorizedParties.includes(azp)) {
|
|
throw new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenInvalidAuthorizedParties,
|
|
message: `Invalid JWT Authorized party claim (azp) ${JSON.stringify(azp)}. Expected "${authorizedParties}".`
|
|
});
|
|
}
|
|
};
|
|
var assertExpirationClaim = (exp, clockSkewInMs) => {
|
|
if (typeof exp !== "number") {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Invalid JWT expiry date claim (exp) ${JSON.stringify(exp)}. Expected number.`
|
|
});
|
|
}
|
|
const currentDate = new Date(Date.now());
|
|
const expiryDate = /* @__PURE__ */ new Date(0);
|
|
expiryDate.setUTCSeconds(exp);
|
|
const expired = expiryDate.getTime() <= currentDate.getTime() - clockSkewInMs;
|
|
if (expired) {
|
|
throw new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenExpired,
|
|
message: `JWT is expired. Expiry date: ${expiryDate.toUTCString()}, Current date: ${currentDate.toUTCString()}.`
|
|
});
|
|
}
|
|
};
|
|
var assertActivationClaim = (nbf, clockSkewInMs) => {
|
|
if (typeof nbf === "undefined") {
|
|
return;
|
|
}
|
|
if (typeof nbf !== "number") {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Invalid JWT not before date claim (nbf) ${JSON.stringify(nbf)}. Expected number.`
|
|
});
|
|
}
|
|
const currentDate = new Date(Date.now());
|
|
const notBeforeDate = /* @__PURE__ */ new Date(0);
|
|
notBeforeDate.setUTCSeconds(nbf);
|
|
const early = notBeforeDate.getTime() > currentDate.getTime() + clockSkewInMs;
|
|
if (early) {
|
|
throw new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenNotActiveYet,
|
|
message: `JWT cannot be used prior to not before date claim (nbf). Not before date: ${notBeforeDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
|
|
});
|
|
}
|
|
};
|
|
var assertIssuedAtClaim = (iat, clockSkewInMs) => {
|
|
if (typeof iat === "undefined") {
|
|
return;
|
|
}
|
|
if (typeof iat !== "number") {
|
|
throw new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Invalid JWT issued at date claim (iat) ${JSON.stringify(iat)}. Expected number.`
|
|
});
|
|
}
|
|
const currentDate = new Date(Date.now());
|
|
const issuedAtDate = /* @__PURE__ */ new Date(0);
|
|
issuedAtDate.setUTCSeconds(iat);
|
|
const postIssued = issuedAtDate.getTime() > currentDate.getTime() + clockSkewInMs;
|
|
if (postIssued) {
|
|
throw new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenIatInTheFuture,
|
|
message: `JWT issued at date claim (iat) is in the future. Issued at date: ${issuedAtDate.toUTCString()}; Current date: ${currentDate.toUTCString()};`
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/jwt/cryptoKeys.ts
|
|
import { isomorphicAtob } from "@clerk/shared/isomorphicAtob";
|
|
function pemToBuffer(secret) {
|
|
const trimmed = secret.replace(/-----BEGIN.*?-----/g, "").replace(/-----END.*?-----/g, "").replace(/\s/g, "");
|
|
const decoded = isomorphicAtob(trimmed);
|
|
const buffer = new ArrayBuffer(decoded.length);
|
|
const bufView = new Uint8Array(buffer);
|
|
for (let i = 0, strLen = decoded.length; i < strLen; i++) {
|
|
bufView[i] = decoded.charCodeAt(i);
|
|
}
|
|
return bufView;
|
|
}
|
|
function importKey(key, algorithm, keyUsage) {
|
|
if (typeof key === "object") {
|
|
return runtime.crypto.subtle.importKey("jwk", key, algorithm, false, [keyUsage]);
|
|
}
|
|
const keyData = pemToBuffer(key);
|
|
const format = keyUsage === "sign" ? "pkcs8" : "spki";
|
|
return runtime.crypto.subtle.importKey(format, keyData, algorithm, false, [keyUsage]);
|
|
}
|
|
|
|
// src/jwt/verifyJwt.ts
|
|
var DEFAULT_CLOCK_SKEW_IN_MS = 5 * 1e3;
|
|
async function hasValidSignature(jwt, key) {
|
|
const { header, signature, raw } = jwt;
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode([raw.header, raw.payload].join("."));
|
|
const algorithm = getCryptoAlgorithm(header.alg);
|
|
try {
|
|
const cryptoKey = await importKey(key, algorithm, "verify");
|
|
const verified = await runtime.crypto.subtle.verify(algorithm.name, cryptoKey, signature, data);
|
|
return { data: verified };
|
|
} catch (error) {
|
|
return {
|
|
errors: [
|
|
new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
message: error?.message
|
|
})
|
|
]
|
|
};
|
|
}
|
|
}
|
|
function decodeJwt(token) {
|
|
const tokenParts = (token || "").toString().split(".");
|
|
if (tokenParts.length !== 3) {
|
|
return {
|
|
errors: [
|
|
new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenInvalid,
|
|
message: `Invalid JWT form. A JWT consists of three parts separated by dots.`
|
|
})
|
|
]
|
|
};
|
|
}
|
|
const [rawHeader, rawPayload, rawSignature] = tokenParts;
|
|
const decoder = new TextDecoder();
|
|
const header = JSON.parse(decoder.decode(base64url.parse(rawHeader, { loose: true })));
|
|
const payload = JSON.parse(decoder.decode(base64url.parse(rawPayload, { loose: true })));
|
|
const signature = base64url.parse(rawSignature, { loose: true });
|
|
const data = {
|
|
header,
|
|
payload,
|
|
signature,
|
|
raw: {
|
|
header: rawHeader,
|
|
payload: rawPayload,
|
|
signature: rawSignature,
|
|
text: token
|
|
}
|
|
};
|
|
return { data };
|
|
}
|
|
async function verifyJwt(token, options) {
|
|
const { audience, authorizedParties, clockSkewInMs, key, headerType } = options;
|
|
const clockSkew = typeof clockSkewInMs === "number" && Number.isFinite(clockSkewInMs) ? clockSkewInMs : DEFAULT_CLOCK_SKEW_IN_MS;
|
|
const { data: decoded, errors } = decodeJwt(token);
|
|
if (errors) {
|
|
return { errors };
|
|
}
|
|
const { header, payload } = decoded;
|
|
try {
|
|
const { typ, alg } = header;
|
|
assertHeaderType(typ, headerType);
|
|
assertHeaderAlgorithm(alg);
|
|
} catch (err) {
|
|
return { errors: [err] };
|
|
}
|
|
const { data: signatureValid, errors: signatureErrors } = await hasValidSignature(decoded, key);
|
|
if (signatureErrors) {
|
|
return {
|
|
errors: [
|
|
new TokenVerificationError({
|
|
action: TokenVerificationErrorAction.EnsureClerkJWT,
|
|
reason: TokenVerificationErrorReason.TokenVerificationFailed,
|
|
message: `Error verifying JWT signature. ${signatureErrors[0]}`
|
|
})
|
|
]
|
|
};
|
|
}
|
|
if (!signatureValid) {
|
|
return {
|
|
errors: [
|
|
new TokenVerificationError({
|
|
reason: TokenVerificationErrorReason.TokenInvalidSignature,
|
|
message: "JWT signature is invalid."
|
|
})
|
|
]
|
|
};
|
|
}
|
|
try {
|
|
const { azp, sub, aud, iat, exp, nbf } = payload;
|
|
assertSubClaim(sub);
|
|
assertAudienceClaim([aud], [audience]);
|
|
assertAuthorizedPartiesClaim(azp, authorizedParties);
|
|
assertExpirationClaim(exp, clockSkew);
|
|
assertActivationClaim(nbf, clockSkew);
|
|
assertIssuedAtClaim(iat, clockSkew);
|
|
} catch (err) {
|
|
return { errors: [err] };
|
|
}
|
|
return { data: payload };
|
|
}
|
|
|
|
export {
|
|
runtime,
|
|
base64url,
|
|
getCryptoAlgorithm,
|
|
assertHeaderType,
|
|
assertHeaderAlgorithm,
|
|
importKey,
|
|
hasValidSignature,
|
|
decodeJwt,
|
|
verifyJwt
|
|
};
|
|
//# sourceMappingURL=chunk-HVNR6UQP.mjs.map
|