Signing
sign(payload, key, options?)
Produces a compact JWS — the familiar dotted three-part string.
Parameters
| Name | Type | Required? |
|---|---|---|
payload | string | Uint8Array | Record<string, unknown> | Yes |
key | CryptoKey | JWSSignJWK | Uint8Array | Yes |
options.alg | JWSAlgorithm | When key has no alg hint |
options.protectedHeader | JWSHeaderParameters (extra fields) | No |
options.expiresIn | number | string — e.g. "1h", "7D", 3600 | No |
options.currentDate | Date | No |
JWSSignJWK narrows the JWK by family: JWK_oct<JWK_HMAC> for HMAC, or an asymmetric private JWK whose alg is in the matching signing family (RS*/PS* for RSA, ES* for ECDSA, Ed25519/EdDSA for OKP). A JWK whose alg points at a non-signing family ("RSA-OAEP", "A256KW", "ECDH-ES", …) is rejected at the type level.
Returns Promise<string> — the compact JWS.
Algorithm inference
When the key is a JWK with an alg field set, unjwt reads it and uses that algorithm. generateJWK() always sets alg for you, so in practice you rarely specify it by hand:
const key = await generateJWK("HS256"); // key.alg === "HS256"
const token = await sign({ sub: "u1" }, key); // uses HS256 automatically
You must pass options.alg explicitly in three cases:
- The key is a raw
Uint8Array(no metadata to infer from). - The key is a
CryptoKeywithout algorithm context (rare —generateKey()sets the algorithm internally). - The JWK has no
algfield.
import { base64UrlDecode } from "unjwt/utils";
const rawKey = base64UrlDecode("GawgguFyGrWKav7AX4VKUg", { returnAs: "uint8array" });
const token = await sign({ sub: "u1" }, rawKey, { alg: "HS256" });
expiresIn — setting exp declaratively
Sets the exp claim relative to iat:
await sign({ sub: "u1" }, key, { expiresIn: "1h" }); // exp = iat + 3600
await sign({ sub: "u1" }, key, { expiresIn: 30 }); // exp = iat + 30
await sign({ sub: "u1" }, key, { expiresIn: "7days" }); // exp = iat + 604800
Accepted forms: "30s", "10m", "2h", "7D", "1W", "3M", "1Y" and the long forms ("seconds", "minutes", "hours", "days", "weeks", "months", "years").
If your payload already has an iat, it's preserved; otherwise the current time is used. Use currentDate to override "now" in tests.
expiresIn is a signer-side option. On the verifier side, pass maxTokenAge or rely on the exp claim that's already baked into the token.Custom header parameters
Anything passed in protectedHeader is merged into the JWS header. alg is reserved — it's always derived from the top-level alg option (or inferred from the key) and can't be overridden here. b64 is user-settable: set it to false to opt into the RFC 7797 unencoded-payload mode (see below).
const token = await sign({ sub: "u1" }, key, {
protectedHeader: {
kid: "legacy-2024", // overrides key.kid if set
typ: "access+jwt", // RFC 8725 guidance — custom type name
cty: "application/json", // nested-content media type
},
});
kid fallback
If the key is a JWK with a kid field and you don't set protectedHeader.kid, unjwt adds kid to the header automatically. This is the "make tokens carry their key identity" behavior — essential for JWKS endpoints and key rotation. An explicit protectedHeader.kid always wins.
b64: false — RFC 7797 unencoded payload
RFC 7797 defines a variant where the JWS payload is signed over raw bytes rather than the base64url-encoded form. Useful when:
- The payload is already a large or structured document (XML, JSON-LD) that consumers want to read without base64 decoding.
- The payload contains binary data and base64url overhead matters.
const token = await sign("important document bytes", key, {
protectedHeader: { b64: false, crit: ["b64"] },
});
// Result: "eyJhbGc...crit...b64...".<raw payload>.<signature>
// (note the payload is literally in the middle)
Always pair b64: false with crit: ["b64"] so verifiers that don't recognize the parameter reject the token instead of mis-validating it.
b64. See Multi-signature.Payload types
sign() accepts three payload shapes:
| Shape | Behavior |
|---|---|
Record<string, unknown> | Serialized to JSON. typ: "JWT" added automatically if not set. |
string | UTF-8 encoded as-is. No typ defaulting — you're not producing a JWT. |
Uint8Array | Used as-is. No typ defaulting. |
The type is inferred so verify() returns the same shape (see forceUint8Array if you want bytes back).
Full signature
interface JWSSignOptions {
alg?: JWSAlgorithm;
protectedHeader?: StrictOmit<JWSHeaderParameters, "alg"> & { alg?: never };
currentDate?: Date;
expiresIn?: ExpiresIn;
expiresAt?: Date;
notBeforeIn?: ExpiresIn;
notBeforeAt?: Date;
}
expiresIn and expiresAt are mutually exclusive; notBeforeIn and notBeforeAt are likewise mutually exclusive. notBeforeIn: 0 is allowed and sets nbf = iat (an explicit temporal floor at sign time).
See also
- Verifying → — the consumer side.
- Algorithms → — which
algto pick. - Multi-signature → — more than one signer.