UNPKG

@tsndr/cloudflare-worker-jwt

Version:

A lightweight JWT implementation with ZERO dependencies for Cloudflare Worker

163 lines (161 loc) 6.89 kB
// 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 };