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

OptionTypeDefaultWhat it does
algJWSAlgorithmthe JWK's algThe signing algorithm. Required for a CryptoKey or raw bytes.
protectedHeaderJWSHeaderParametersnoneExtra header members (kid, typ, cty, crit, b64, your own). alg cannot be set here.
expiresInDurationnoneSets exp to iat plus the duration. Exclusive with expiresAt.
expiresAtDatenoneSets exp to that instant.
notBeforeInDurationnoneSets nbf to iat plus the duration. Exclusive with notBeforeAt.
notBeforeAtDatenoneSets nbf to that instant.
currentDateDatenew Date()The moment used for iat.

#The payload

The payload type decides what sign adds:

PayloadEncoded astyp addedTime options apply
An objectits JSON text"JWT"yes
A stringits UTF-8 bytesnono
A Uint8Arraythe bytes as givennono

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 generateJWK or importKey. alg comes from the JWK, and its kid is copied into the header. An asymmetric JWK must be the private half: a public one is ERR_JWK_INVALID ("Public JWK passed where a private key is expected.").
  • A CryptoKey. Pass alg. No kid is added, so set one in protectedHeader if verifiers select keys by it.
  • Raw bytes. A Uint8Array is an HMAC secret. Pass an HS* alg. The secret must be at least as long as the hash: 32 bytes for HS256, 48 for HS384, 64 for HS512 (RFC 7518 §3.2).
  • A JWK Set is not a signing key. sign uses one key, so pick the entry first.
  • A string is never a key. Import PEM text with importPEM and pass an oct JWK 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" }
  • typ defaults to "JWT" for an object payload. A typ you set is kept. It must be a string.
  • cty is never added. Set cty: "JWT" when the payload is itself a token (RFC 7519 §5.2).
  • crit must be a non-empty array of strings. It may not list a registered name such as kid or typ, and every entry must be a member of the header you are building. Receivers reject a token whose crit lists 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

CodeWhen
ERR_JWS_ALG_MISSINGNo alg option and the key is not a JWK that declares one.
ERR_JWS_ALG_NOT_ALLOWEDalg is "none" or outside the supported list.
ERR_JWK_INVALIDA 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_OPTIONoptions 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_INVALIDBoth expiresIn and expiresAt, both notBeforeIn and notBeforeAt, or a negative notBeforeIn.
ERR_JWS_INVALIDA b64: false payload that contains . or is not valid UTF-8.

#See also