better-auth
Version:
The most comprehensive authentication framework for TypeScript.
142 lines (141 loc) • 3.7 kB
JavaScript
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 };