UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

142 lines (141 loc) 3.7 kB
import { hkdf } from "@noble/hashes/hkdf.js"; import { sha256 } from "@noble/hashes/sha2.js"; import { EncryptJWT, SignJWT, base64url, calculateJwkThumbprint, decodeProtectedHeader, jwtDecrypt, jwtVerify } from "jose"; //#region src/crypto/jwt.ts async function signJWT(payload, secret, expiresIn = 3600) { return await new SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(Math.floor(Date.now() / 1e3) + expiresIn).sign(new TextEncoder().encode(secret)); } async function verifyJWT(token, secret) { try { return (await jwtVerify(token, new TextEncoder().encode(secret))).payload; } catch { return null; } } const info = new Uint8Array([ 66, 101, 116, 116, 101, 114, 65, 117, 116, 104, 46, 106, 115, 32, 71, 101, 110, 101, 114, 97, 116, 101, 100, 32, 69, 110, 99, 114, 121, 112, 116, 105, 111, 110, 32, 75, 101, 121 ]); const now = () => Date.now() / 1e3 | 0; const alg = "dir"; const enc = "A256CBC-HS512"; function deriveEncryptionSecret(secret, salt) { return hkdf(sha256, new TextEncoder().encode(secret), new TextEncoder().encode(salt), info, 64); } function getCurrentSecret(secret) { if (typeof secret === "string") return secret; const value = secret.keys.get(secret.currentVersion); if (!value) throw new Error(`Secret version ${secret.currentVersion} not found in keys`); return value; } function getAllSecrets(secret) { if (typeof secret === "string") return [{ version: 0, value: secret }]; const result = []; for (const [version, value] of secret.keys) result.push({ version, value }); if (secret.legacySecret && !result.some((s) => s.value === secret.legacySecret)) result.push({ version: -1, value: secret.legacySecret }); return result; } async function symmetricEncodeJWT(payload, secret, salt, expiresIn = 3600) { const encryptionSecret = deriveEncryptionSecret(getCurrentSecret(secret), salt); const thumbprint = await calculateJwkThumbprint({ kty: "oct", k: base64url.encode(encryptionSecret) }, "sha256"); return await new EncryptJWT(payload).setProtectedHeader({ alg, enc, kid: thumbprint }).setIssuedAt().setExpirationTime(now() + expiresIn).setJti(crypto.randomUUID()).encrypt(encryptionSecret); } const jwtDecryptOpts = { clockTolerance: 15, keyManagementAlgorithms: [alg], contentEncryptionAlgorithms: [enc, "A256GCM"] }; async function symmetricDecodeJWT(token, secret, salt) { if (!token) return null; let hasKid = false; try { hasKid = decodeProtectedHeader(token).kid !== void 0; } catch { return null; } try { const secrets = getAllSecrets(secret); const { payload } = await jwtDecrypt(token, async (protectedHeader) => { const kid = protectedHeader.kid; if (kid !== void 0) { for (const s of secrets) { const encryptionSecret = deriveEncryptionSecret(s.value, salt); if (kid === await calculateJwkThumbprint({ kty: "oct", k: base64url.encode(encryptionSecret) }, "sha256")) return encryptionSecret; } throw new Error("no matching decryption secret"); } if (secrets.length === 1) return deriveEncryptionSecret(secrets[0].value, salt); return deriveEncryptionSecret(secrets[0].value, salt); }, jwtDecryptOpts); return payload; } catch { if (hasKid) return null; const secrets = getAllSecrets(secret); if (secrets.length <= 1) return null; for (let i = 1; i < secrets.length; i++) try { const s = secrets[i]; const { payload } = await jwtDecrypt(token, deriveEncryptionSecret(s.value, salt), jwtDecryptOpts); return payload; } catch { continue; } return null; } } //#endregion export { signJWT, symmetricDecodeJWT, symmetricEncodeJWT, verifyJWT };