generateJWK() / generateKey()
Make a fresh key for any JWS or JWE algorithm. generateJWK returns JSON ready to store; generateKey returns Web Crypto objects unless you ask for JSON.
import { generateJWK } from "unjwt/jwk";
const secret = await generateJWK("HS256");
// {
// alg: "HS256",
// ext: true,
// k: "sQJeXE6RCGCL0PQ-oG9nK3HW…",
// key_ops: ["sign", "verify"],
// kty: "oct",
// kid: "Z7Vbm5-ga-jt60sVMc9u6T5_V5uPrO_dUQNw-6uVeBM",
// }
const { privateKey, publicKey } = await generateJWK("ES256");
// privateKey: { crv: "P-256", d: "V5UCmgY_…", x: "mGQhkJr2…", y: "m-i6Bvx2…", kty: "EC",
// key_ops: ["sign"], ext: true, alg: "ES256", kid: "vlj-lU5hMsOLA09r…" }
// publicKey: the same without `d`, with key_ops: ["verify"] and the same kidReach for generateJWK by default. Use generateKey when you want a CryptoKey that never leaves the process (see Non-extractable keys).
#Signatures
function generateJWK<TAlg extends GenerateKeyAlgorithm, Kid extends string | false = string>(
alg: TAlg,
options?: {
"kid"?: Kid;
"keyUsage"?: KeyUsage[];
"modulusLength"?: number;
"publicExponent"?: Uint8Array;
"namedCurve"?: "P-256" | "P-384" | "P-521" | "X25519" | "Ed25519";
// JWK metadata merged onto the result
"use"?: string;
"x5c"?: string[];
"x5t"?: string;
"x5t#S256"?: string;
"x5u"?: string;
},
): Promise<GeneratedJWKMap<Kid>[TAlg]>;
function generateKey<TAlg extends GenerateKeyAlgorithm>(
alg: TAlg,
options?: {
toJWK?: boolean;
extractable?: boolean;
kid?: string | false;
keyUsage?: KeyUsage[];
modulusLength?: number;
publicExponent?: Uint8Array;
namedCurve?: "P-256" | "P-384" | "P-521" | "X25519" | "Ed25519";
},
): Promise<GeneratedKeyMap[TAlg] | GeneratedJWKMap[TAlg]>;GenerateKeyAlgorithm is every JWS and JWE algorithm except "none", "dir" and the PBES2 family. A password-based key comes from deriveJWKFromPassword().
#Options
| Option | Type | Default | What it does |
|---|---|---|---|
kid | string | false | the RFC 7638 thumbprint | A string is set verbatim. false sets no kid. Anything else is ERR_INVALID_OPTION. On generateKey it is read only with toJWK: true. |
toJWK | boolean | false | generateKey only. Return JWKs instead of Web Crypto objects. |
extractable | boolean | true | generateKey only. Ignored with toJWK, which must export the key. |
keyUsage | KeyUsage[] | derived from alg | Narrows the usages. A usage the algorithm cannot serve, or a list that leaves one half of a pair with nothing, is ERR_INVALID_OPTION. |
modulusLength | number | 2048 | RSA only. Below 2048 bits is ERR_INVALID_OPTION (RFC 7518 §3.3). |
publicExponent | Uint8Array | 0x010001 | RSA only. Big-endian bytes. |
namedCurve | see signature | "P-256" for EC, "Ed25519" for OKP | ES256/ES384/ES512 fix their curve, so a different one is ERR_INVALID_OPTION. ECDH-ES* picks from P-256, P-384, P-521 and X25519. EdDSA accepts Ed25519 only. |
use, x5c, x5t, x5t#S256, x5u | JWK members | none | generateJWK only. Merged onto the key (both halves of a pair). alg, kty, key_ops, ext and key material are reserved and throw ERR_INVALID_OPTION. |
#Return shape
The algorithm decides the shape. Every JWK carries alg and, unless kid: false, a kid.
| Algorithm | generateKey(alg) | generateJWK(alg) or generateKey(alg, { toJWK: true }) |
|---|---|---|
HS*, A*KW, A*GCM, A*GCMKW | CryptoKey | one oct JWK |
A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 | Uint8Array (Web Crypto has no such key type) | one oct JWK |
RS*, PS*, RSA-OAEP* | CryptoKeyPair | { privateKey, publicKey }, kty: "RSA" |
ES* | CryptoKeyPair | { privateKey, publicKey }, kty: "EC" |
Ed25519, EdDSA | CryptoKeyPair | { privateKey, publicKey }, kty: "OKP" |
ECDH-ES* | CryptoKeyPair | { privateKey, publicKey }, kty: "EC", or "OKP" for X25519 |
The table is two exported types, GeneratedKeyMap and GeneratedJWKMap, keyed by algorithm. A literal alg narrows the result, so no cast is needed:
import { generateJWK, generateKey } from "unjwt/jwk";
const a = await generateJWK("HS256");
// typeof a: JWK_oct<"HS256"> & { alg: "HS256"; kid: string }
const b = await generateJWK("HS256", { kid: false });
// typeof b: JWK_oct<"HS256"> & { alg: "HS256" }
const c = await generateJWK("ES256");
// typeof c: {
// privateKey: JWK_EC_Private<"ES256"> & { alg: "ES256"; kid: string };
// publicKey: JWK_EC_Public<"ES256"> & { alg: "ES256"; kid: string };
// }
const d = await generateKey("HS256"); // typeof d: CryptoKey
const e = await generateKey("ES256"); // typeof e: CryptoKeyPair
const f = await generateKey("A128CBC-HS256"); // typeof f: Uint8Array<ArrayBuffer>
const g = await generateKey("ES256", { toJWK: true }); // typeof g: GeneratedJWKMap["ES256"]A non-literal algorithm (a variable typed GenerateKeyAlgorithm) returns the union of its entries.
#The default kid is a thumbprint
A kid is the name a token carries in its header so a verifier can find the right key. When you do not pass one, the key is named by its RFC 7638 thumbprint: a SHA-256 digest of the key's required public members. Two consequences follow. Both halves of a pair get the same kid, because the private members are not part of the digest. And two different keys never share a kid by accident.
import { generateJWK, thumbprint } from "unjwt/jwk";
const key = await generateJWK("HS256");
key.kid === (await thumbprint(key)); // true
await generateJWK("HS256", { kid: "hmac-2026-10" }); // kid: "hmac-2026-10"
await generateJWK("HS256", { kid: false }); // no kid member at allthumbprint(jwk, { hash }) is exported too, with hash one of "SHA-256" (default), "SHA-384" or "SHA-512". Passing your own kid, or false, skips the digest.
#Wrapping keys get the alg you asked for
generateJWK("A256GCMKW") returns a key with alg: "A256GCMKW". Web Crypto itself only knows AES-GCM, so exporting the same key through exportKey() labels it A256GCM, the content encryption role. Mint key-wrapping keys with generateJWK so the label matches the job.
#Non-extractable keys
generateKey defaults to extractable: true so keys can be exported. For a long-lived private key that should never leave the process, ask for a non-extractable pair:
import { generateKey } from "unjwt/jwk";
const pair = await generateKey("ES256", { extractable: false });
pair.privateKey.extractable; // false
pair.publicKey.extractable; // true, Web Crypto always keeps public keys exportableThe private CryptoKey still signs, but its material cannot be read back out.
#Errors
| Cause | code |
|---|---|
An algorithm with no generated key (PBES2-*, dir, none, anything unknown) | ERR_JWK_UNSUPPORTED |
EdDSA with a curve other than Ed25519, or ECDH-ES* with an unknown curve | ERR_JWK_UNSUPPORTED |
modulusLength below 2048, a malformed publicExponent | ERR_INVALID_OPTION |
A namedCurve that contradicts ES* or Ed25519 | ERR_INVALID_OPTION |
A keyUsage the algorithm cannot serve, or one that empties a pair half | ERR_INVALID_OPTION |
A kid that is neither a string nor false, or a reserved JWK member in the metadata | ERR_INVALID_OPTION |
await generateJWK("ES256", { keyUsage: ["sign"] });
// JWTError ERR_INVALID_OPTION: "keyUsage" leaves the public half of a ES256 key pair with no usage; include one of verify.#See also
- importKey() / exportKey() to convert what you generated.
- JWK Sets to publish public halves and rotate.
- JWS algorithms and JWE algorithms to choose an
alg.