
# 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.

```ts
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

```ts
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`.

```ts
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`](/jwk/generate) or [`importKey`](/jwk/import-export). `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](https://www.rfc-editor.org/rfc/rfc7518#section-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`](/jwk/pem) and pass an `oct` JWK or bytes for a secret.

```ts
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`.

```ts
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](https://www.rfc-editor.org/rfc/rfc7519#section-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.

```ts
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](https://www.rfc-editor.org/rfc/rfc7797) lets the payload travel as plain text instead of base64url. `sign` adds `"b64"` to `crit` for you, because the RFC requires it.

```ts
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`](/jwt/jws/multi) 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](/jwt/jws/algorithms).                                                                                                                                     |
| `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()`](/jwt/jws/verify) for the receiving side.
- [Algorithms](/jwt/jws/algorithms) for picking `alg`.
- [`generateJWK`](/jwk/generate) for keys that carry `alg` and `kid`.
