@tsndr/cloudflare-worker-jwt
Version:
A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker
163 lines (161 loc) • 6.89 kB
JavaScript
// src/utils.ts
function bytesToByteString(bytes) {
let byteStr = "";
for (let i = 0; i < bytes.byteLength; i++) {
byteStr += String.fromCharCode(bytes[i]);
}
return byteStr;
}
function byteStringToBytes(byteStr) {
let bytes = new Uint8Array(byteStr.length);
for (let i = 0; i < byteStr.length; i++) {
bytes[i] = byteStr.charCodeAt(i);
}
return bytes;
}
function arrayBufferToBase64String(arrayBuffer) {
return btoa(bytesToByteString(new Uint8Array(arrayBuffer)));
}
function base64StringToUint8Array(b64str) {
return byteStringToBytes(atob(b64str));
}
function textToUint8Array(str) {
return byteStringToBytes(str);
}
function arrayBufferToBase64Url(arrayBuffer) {
return arrayBufferToBase64String(arrayBuffer).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function base64UrlToUint8Array(b64url) {
return base64StringToUint8Array(b64url.replace(/-/g, "+").replace(/_/g, "/").replace(/\s/g, ""));
}
function textToBase64Url(str) {
const encoder = new TextEncoder();
const charCodes = encoder.encode(str);
const binaryStr = String.fromCharCode(...charCodes);
return btoa(binaryStr).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function pemToBinary(pem) {
return base64StringToUint8Array(pem.replace(/-+(BEGIN|END).*/g, "").replace(/\s/g, ""));
}
async function importTextSecret(key, algorithm, keyUsages) {
return await crypto.subtle.importKey("raw", textToUint8Array(key), algorithm, true, keyUsages);
}
async function importJwk(key, algorithm, keyUsages) {
return await crypto.subtle.importKey("jwk", key, algorithm, true, keyUsages);
}
async function importPublicKey(key, algorithm, keyUsages) {
return await crypto.subtle.importKey("spki", pemToBinary(key), algorithm, true, keyUsages);
}
async function importPrivateKey(key, algorithm, keyUsages) {
return await crypto.subtle.importKey("pkcs8", pemToBinary(key), algorithm, true, keyUsages);
}
async function importKey(key, algorithm, keyUsages) {
if (typeof key === "object")
return importJwk(key, algorithm, keyUsages);
if (typeof key !== "string")
throw new Error("Unsupported key type!");
if (key.includes("PUBLIC"))
return importPublicKey(key, algorithm, keyUsages);
if (key.includes("PRIVATE"))
return importPrivateKey(key, algorithm, keyUsages);
return importTextSecret(key, algorithm, keyUsages);
}
function decodePayload(raw) {
const bytes = Array.from(atob(raw), (char) => char.charCodeAt(0));
const decodedString = new TextDecoder("utf-8").decode(new Uint8Array(bytes));
return JSON.parse(decodedString);
}
// src/index.ts
if (typeof crypto === "undefined" || !crypto.subtle)
throw new Error("SubtleCrypto not supported!");
var algorithms = {
none: { name: "none" },
ES256: { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
ES384: { name: "ECDSA", namedCurve: "P-384", hash: { name: "SHA-384" } },
ES512: { name: "ECDSA", namedCurve: "P-521", hash: { name: "SHA-512" } },
HS256: { name: "HMAC", hash: { name: "SHA-256" } },
HS384: { name: "HMAC", hash: { name: "SHA-384" } },
HS512: { name: "HMAC", hash: { name: "SHA-512" } },
RS256: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-256" } },
RS384: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-384" } },
RS512: { name: "RSASSA-PKCS1-v1_5", hash: { name: "SHA-512" } }
};
async function sign(payload, secret, options = "HS256") {
if (typeof options === "string")
options = { algorithm: options };
options = { algorithm: "HS256", header: { typ: "JWT", ...options.header ?? {} }, ...options };
if (!payload || typeof payload !== "object")
throw new Error("payload must be an object");
if (options.algorithm !== "none" && (!secret || typeof secret !== "string" && typeof secret !== "object"))
throw new Error("secret must be a string, a JWK object or a CryptoKey object");
if (typeof options.algorithm !== "string")
throw new Error("options.algorithm must be a string");
const algorithm = algorithms[options.algorithm];
if (!algorithm)
throw new Error("algorithm not found");
if (!payload.iat)
payload.iat = Math.floor(Date.now() / 1e3);
const partialToken = `${textToBase64Url(JSON.stringify({ ...options.header, alg: options.algorithm }))}.${textToBase64Url(JSON.stringify(payload))}`;
if (options.algorithm === "none")
return partialToken;
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ["sign"]);
const signature = await crypto.subtle.sign(algorithm, key, textToUint8Array(partialToken));
return `${partialToken}.${arrayBufferToBase64Url(signature)}`;
}
async function verify(token, secret, options = "HS256") {
if (typeof options === "string")
options = { algorithm: options };
options = { algorithm: "HS256", clockTolerance: 0, throwError: false, ...options };
if (typeof token !== "string")
throw new Error("token must be a string");
if (options.algorithm !== "none" && typeof secret !== "string" && typeof secret !== "object")
throw new Error("secret must be a string, a JWK object or a CryptoKey object");
if (typeof options.algorithm !== "string")
throw new Error("options.algorithm must be a string");
const tokenParts = token.split(".", 3);
if (tokenParts.length < 2)
throw new Error("token must consist of 2 or more parts");
const [tokenHeader, tokenPayload, tokenSignature] = tokenParts;
const algorithm = algorithms[options.algorithm];
if (!algorithm)
throw new Error("algorithm not found");
const decodedToken = decode(token);
try {
if (decodedToken.header?.alg !== options.algorithm)
throw new Error("INVALID_SIGNATURE");
if (decodedToken.payload) {
const now = Math.floor(Date.now() / 1e3);
if (decodedToken.payload.nbf && decodedToken.payload.nbf > now && decodedToken.payload.nbf - now > (options.clockTolerance ?? 0))
throw new Error("NOT_YET_VALID");
if (decodedToken.payload.exp && decodedToken.payload.exp <= now && now - decodedToken.payload.exp > (options.clockTolerance ?? 0))
throw new Error("EXPIRED");
}
if (algorithm.name === "none")
return decodedToken;
const key = secret instanceof CryptoKey ? secret : await importKey(secret, algorithm, ["verify"]);
if (!await crypto.subtle.verify(algorithm, key, base64UrlToUint8Array(tokenSignature), textToUint8Array(`${tokenHeader}.${tokenPayload}`)))
throw new Error("INVALID_SIGNATURE");
return decodedToken;
} catch (err) {
if (options.throwError)
throw err;
return;
}
}
function decode(token) {
return {
header: decodePayload(token.split(".")[0].replace(/-/g, "+").replace(/_/g, "/")),
payload: decodePayload(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
};
}
var index_default = {
sign,
verify,
decode
};
export {
decode,
index_default as default,
sign,
verify
};