better-auth
Version:
The most comprehensive authentication framework for TypeScript.
131 lines (130 loc) • 6.18 kB
JavaScript
import { symmetricDecrypt } from "../../crypto/index.mjs";
import { getJwksAdapter } from "./adapter.mjs";
import { createJwk, toExpJWT } from "./utils.mjs";
import { BetterAuthError } from "@better-auth/core/error";
import { SignJWT, importJWK } from "jose";
//#region src/plugins/jwt/sign.ts
/**
* Resolves the JWKS signing key, decrypts it, and imports it
* for use with jose's SignJWT. Returns null when signing is
* delegated to a custom jwt.sign callback.
*
* Callers that need the signing algorithm before constructing
* the JWT payload (e.g. for OIDC at_hash) should call this
* first, read `.alg`, then pass the result to `signJWT` via
* the `resolvedKey` option to avoid a redundant DB lookup.
*
* When `overrides.signingKeyId` or `overrides.signingAlgorithm` is set, key
* selection follows the contract documented on {@link SigningKeyOverrides};
* without overrides this returns the most recently created live key, falling
* back to the primary `keyPairConfig.alg` when even that's absent so unpinned
* tokens stay on the configured default algorithm even after extra algorithms
* have been lazy-minted for audience pinning.
*/
async function resolveSigningKey(ctx, options, overrides) {
if (options?.jwt?.sign) return null;
const adapter = getJwksAdapter(ctx.context.adapter, options);
let key;
if (overrides?.signingKeyId !== void 0) {
key = await adapter.getKeyById(ctx, overrides.signingKeyId);
if (!key) throw new BetterAuthError(`signJWT: signingKeyId "${overrides.signingKeyId}" not found in JWKS. The key must be provisioned before it can be referenced.`);
if (overrides.signingAlgorithm !== void 0) {
const configAlg = options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
if ((key.alg ?? configAlg) !== overrides.signingAlgorithm) throw new BetterAuthError(`signJWT: signingKeyId "${overrides.signingKeyId}" has alg "${key.alg ?? `unset (inherits keyPairConfig.alg "${configAlg}")`}" but signingAlgorithm was set to "${overrides.signingAlgorithm}".`);
}
} else if (overrides?.signingAlgorithm !== void 0) {
key = await adapter.getLatestKeyByAlg(ctx, overrides.signingAlgorithm);
if (!key) {
const primaryAlg = options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
const preconfig = options?.jwks?.keyPairConfigs?.find((c) => c.alg === overrides.signingAlgorithm);
const isPrimary = primaryAlg === overrides.signingAlgorithm;
if (preconfig || isPrimary) key = await createJwk(ctx, {
...options,
jwks: {
...options?.jwks,
keyPairConfig: preconfig ?? options?.jwks?.keyPairConfig
}
});
else {
const advertisedExtra = options?.jwks?.keyPairConfigs?.map((c) => c.alg).join(", ") || "none";
throw new BetterAuthError(`signJWT: no key with alg "${overrides.signingAlgorithm}" found in JWKS. The plugin auto-mints only one key matching keyPairConfig.alg="${primaryAlg}"; additional algs configured via keyPairConfigs: ${advertisedExtra}. Add "${overrides.signingAlgorithm}" to jwks.keyPairConfigs so the plugin provisions it on first use, or mint the key explicitly via createJwk().`);
}
}
} else {
const primaryAlg = options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
key = await adapter.getLatestKeyByAlg(ctx, primaryAlg) ?? await adapter.getLatestKey(ctx);
}
if (!key || key.expiresAt && key.expiresAt < /* @__PURE__ */ new Date()) {
if (overrides?.signingKeyId !== void 0 || overrides?.signingAlgorithm !== void 0) throw new BetterAuthError("signJWT: requested signing key is expired and an explicit kid/alg was provided; not auto-minting a replacement. Rotate the key explicitly.");
key = await createJwk(ctx, options);
}
const privateWebKey = !options?.jwks?.disablePrivateKeyEncryption ? await symmetricDecrypt({
key: ctx.context.secretConfig,
data: JSON.parse(key.privateKey)
}).catch(() => {
throw new BetterAuthError("Failed to decrypt private key. Make sure the secret currently in use is the same as the one used to encrypt the private key. If you are using a different secret, either clean up your JWKS or disable private key encryption.");
}) : key.privateKey;
const alg = key.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA";
const privateKey = await importJWK(JSON.parse(privateWebKey), alg);
return {
alg,
kid: key.id,
privateKey
};
}
async function signJWT(ctx, config) {
const { options } = config;
const payload = config.payload;
const nowSeconds = Math.floor(Date.now() / 1e3);
const iat = payload.iat;
let exp = payload.exp;
const defaultExp = toExpJWT(options?.jwt?.expirationTime ?? "15m", iat ?? nowSeconds);
exp = exp ?? defaultExp;
const nbf = payload.nbf;
const baseURLOrigin = typeof ctx.context.options.baseURL === "string" ? ctx.context.options.baseURL : "";
const iss = payload.iss;
const defaultIss = options?.jwt?.issuer ?? baseURLOrigin;
const aud = payload.aud;
const defaultAud = options?.jwt?.audience ?? baseURLOrigin;
if (options?.jwt?.sign) {
const jwtPayload = {
...payload,
iat,
exp,
nbf,
iss: iss ?? defaultIss,
aud: aud ?? defaultAud
};
return options.jwt.sign(jwtPayload, config.header, {
signingKeyId: config.signingKeyId,
signingAlgorithm: config.signingAlgorithm
});
}
const { alg, kid, privateKey } = config.resolvedKey ?? await resolveSigningKey(ctx, options, {
signingKeyId: config.signingKeyId,
signingAlgorithm: config.signingAlgorithm
});
const jwt = new SignJWT(payload).setProtectedHeader({
...config.header,
alg,
kid
}).setExpirationTime(exp).setIssuer(iss ?? defaultIss).setAudience(aud ?? defaultAud);
if (iat) jwt.setIssuedAt(iat);
if (payload.sub) jwt.setSubject(payload.sub);
if (payload.nbf) jwt.setNotBefore(payload.nbf);
if (payload.jti) jwt.setJti(payload.jti);
return await jwt.sign(privateKey);
}
async function getJwtToken(ctx, options) {
const payload = !options?.jwt?.definePayload ? ctx.context.session.user : await options.jwt.definePayload(ctx.context.session);
return await signJWT(ctx, {
options,
payload: {
iat: Math.floor(Date.now() / 1e3),
...payload,
sub: await options?.jwt?.getSubject?.(ctx.context.session) ?? ctx.context.session.user.id
}
});
}
//#endregion
export { getJwtToken, resolveSigningKey, signJWT };