sign()
Turn a payload and a signing key into a compact JWS string, with the algorithm, kid and typ filled in from the key and the payload.
import { sign } from "unjwt/jws";
import { generateJWK } from "unjwt/jwk";
const key = await generateJWK("HS256");
// { alg: "HS256", ext: true, k: "Xs0ypr…", key_ops: ["sign", "verify"], kty: "oct", kid: "F6Vju_2n…" }
const token = await sign({ sub: "user_1" }, key, { expiresIn: "1h" });
// "eyJraWQiOiJGNlZqdV8ybm9o…"
// header: { kid: "F6Vju_2n…", alg: "HS256", typ: "JWT" }
// payload: {"sub":"user_1","iat":1792068477,"exp":1792072077}#Signature
function sign(
payload: string | Uint8Array | Record<string, unknown>,
key: JWSSignJWK | CryptoKey | Uint8Array,
options?: JWSSignOptions, // `alg` is required when `key` is a CryptoKey or a Uint8Array
): Promise<string>;JWSSignJWK is an oct JWK with an HS* algorithm, or a private RSA, EC or OKP JWK with a signing algorithm. A JWK whose alg names an encryption algorithm ("RSA-OAEP-256", "A256KW") is a type error.
#Options
| Option | Type | Default | What it does |
|---|---|---|---|
alg | JWSAlgorithm | the JWK's alg | The signing algorithm. Required for a CryptoKey or raw bytes. |
protectedHeader | JWSHeaderParameters | none | Extra header members (kid, typ, cty, crit, b64, your own). alg cannot be set here. |
expiresIn | Duration | none | Sets exp to iat plus the duration. Exclusive with expiresAt. |
expiresAt | Date | none | Sets exp to that instant. |
notBeforeIn | Duration | none | Sets nbf to iat plus the duration. Exclusive with notBeforeAt. |
notBeforeAt | Date | none | Sets nbf to that instant. |
currentDate | Date | new Date() | The moment used for iat. |
#The payload
The payload type decides what sign adds:
| Payload | Encoded as | typ added | Time options apply |
|---|---|---|---|
| An object | its JSON text | "JWT" | yes |
| A string | its UTF-8 bytes | no | no |
A Uint8Array | the bytes as given | no | no |
A time option only adds a claim the object lacks. iat is added when one of them sets a claim. A payload JSON.stringify refuses, such as one holding a BigInt, is ERR_INVALID_OPTION.
const token = await sign({ sub: "u" }, key, {
currentDate: new Date("2026-01-01T00:00:00Z"),
expiresIn: "15m",
notBeforeIn: 0,
});
// payload: {"sub":"u","iat":1767225600,"exp":1767226500,"nbf":1767225600}
await sign({ sub: "u", exp: 1800000000 }, key, { expiresIn: "1h" });
// payload: {"sub":"u","exp":1800000000} (exp kept, so no iat either)#The key
- A JWK from
generateJWKorimportKey.algcomes from the JWK, and itskidis copied into the header. An asymmetric JWK must be the private half: a public one isERR_JWK_INVALID("Public JWK passed where a private key is expected."). - A
CryptoKey. Passalg. Nokidis added, so set one inprotectedHeaderif verifiers select keys by it. - Raw bytes. A
Uint8Arrayis an HMAC secret. Pass anHS*alg. The secret must be at least as long as the hash: 32 bytes forHS256, 48 forHS384, 64 forHS512(RFC 7518 §3.2). - A JWK Set is not a signing key.
signuses one key, so pick the entry first. - A string is never a key. Import PEM text with
importPEMand pass anoctJWK or bytes for a secret.
const secret = crypto.getRandomValues(new Uint8Array(32));
await sign({ sub: "u" }, secret, { alg: "HS256" });
// header: { alg: "HS256", typ: "JWT" }
const jwks = {
keys: [
await generateJWK("HS256", { kid: "2025-12" }),
await generateJWK("HS256", { kid: "2026-01" }),
],
};
const current = jwks.keys.find((k) => k.kid === "2026-01")!;
await sign({ sub: "user_1" }, current);
// header: { kid: "2026-01", alg: "HS256", typ: "JWT" }
await sign({ sub: "u" }, new Uint8Array(16), { alg: "HS256" });
// JWTError ERR_JWK_INVALID: HS256 requires a key of at least 32 bytes (got 16)
await sign({ sub: "u" }, "my-secret", { alg: "HS256" });
// JWTError ERR_JWK_INVALID: String keys are not accepted: pass an oct JWK or a Uint8Array secret, or import PEM text with importPEM().#The protected header
sign builds the header in this order, later steps winning: the JWK's kid, then your protectedHeader, then alg.
await sign({ sub: "u" }, key, { protectedHeader: { typ: "at+jwt", kid: "2026-01" } });
// header: { kid: "2026-01", typ: "at+jwt", alg: "HS256" }
await sign("eyJ…", key, { protectedHeader: { cty: "JWT" } });
// header: { kid: "F6Vju_2n…", cty: "JWT", alg: "HS256" }typdefaults to"JWT"for an object payload. Atypyou set is kept. It must be a string.ctyis never added. Setcty: "JWT"when the payload is itself a token (RFC 7519 §5.2).critmust be a non-empty array of strings. It may not list a registered name such askidortyp, and every entry must be a member of the header you are building. Receivers reject a token whosecritlists something they do not understand, so use it only for extensions they agreed on.
await sign({ sub: "u" }, key, {
protectedHeader: { "crit": ["urn:example:ext"], "urn:example:ext": true },
});
// header: { kid: "F6Vju_2n…", crit: ["urn:example:ext"], "urn:example:ext": true, alg: "HS256", typ: "JWT" }
await sign({ sub: "u" }, key, { protectedHeader: { crit: ["exp-ext"] } });
// JWTError ERR_INVALID_OPTION: "crit" lists "exp-ext", which the protected header does not carry.#Unencoded payload (b64: false)
RFC 7797 lets the payload travel as plain text instead of base64url. sign adds "b64" to crit for you, because the RFC requires it.
const detached = await sign("hello", key, { protectedHeader: { b64: false } });
// ["eyJraWQiOiJGNlZq…", "hello", "cyCgwIp5…"]
// header: { kid: "F6Vju_2n…", b64: false, alg: "HS256", crit: ["b64"] }
await sign("a.b", key, { protectedHeader: { b64: false } });
// JWTError ERR_JWS_INVALID: Invalid JWS: unencoded compact payload must not contain "." (RFC 7797 §5.2).The unencoded payload must be valid UTF-8 and, in the compact form, free of .. Use signMulti for a payload with dots. b64: true is dropped from the header unless crit lists it.
#Errors
| Code | When |
|---|---|
ERR_JWS_ALG_MISSING | No alg option and the key is not a JWK that declares one. |
ERR_JWS_ALG_NOT_ALLOWED | alg is "none" or outside the supported list. |
ERR_JWK_INVALID | A string key, a public key, a short HMAC secret, an RSA key under 2048 bits, raw bytes with a non-HS* alg, or a key made for another alg. The Web Crypto error, if any, is the cause. |
ERR_INVALID_OPTION | options or protectedHeader is not an object, a malformed duration, a non-string typ, a non-boolean b64, a crit that breaks the rules above, or a payload or header that is not JSON-serializable. |
ERR_JWT_CLAIM_INVALID | Both expiresIn and expiresAt, both notBeforeIn and notBeforeAt, or a negative notBeforeIn. |
ERR_JWS_INVALID | A b64: false payload that contains . or is not valid UTF-8. |
#See also
verify()for the receiving side.- Algorithms for picking
alg. generateJWKfor keys that carryalgandkid.