Core concepts
The JOSE specs (short for JavaScript Object Signing and Encryption) use several acronyms that sound alike. This page is the shortest explanation of each that still makes sense.
JWT — JSON Web Token
A JWT is the end product: a small string that carries some JSON data, and is either signed or encrypted so the receiver can trust it.
eyJhbGciOi...eyJzdWIiOi...SflKxwRJSM...
<header> .<payload> .<signature>
Three base64url segments joined by dots. That's the "Compact Serialization" — by far the most common form.
Defined in RFC 7519.
sub, exp, iss, …). The broader term for the same dotted format is JOSE token, and its payload can be any bytes. unjwt handles both — if you pass a string or Uint8Array to sign()/encrypt(), you get a valid JOSE token that isn't technically a JWT.JWS — JSON Web Signature
A JWS is a JWT whose payload is signed. Anyone can read the payload; only the holder of the signing key could have produced the signature.
Use when:
- You want the client to read the data (user ID, role, expiration) but trust it came from you.
- The data is non-sensitive.
Defined in RFC 7515. → JWS in unjwt.
JWE — JSON Web Encryption
A JWE is a JWT whose payload is encrypted. Only the holder of the decryption key can read the data. Anyone else sees opaque ciphertext.
Use when:
- The payload contains sensitive data the client (or intermediary) should not see.
- You want to keep the self-contained, signed-like shape of a JWT but with confidentiality.
Defined in RFC 7516. → JWE in unjwt.
JWK — JSON Web Key
A JWK is a way to represent a cryptographic key as a JSON object. Every signing, verifying, encrypting, and decrypting operation involves a key; a JWK is unjwt's preferred way to pass keys around.
{
"kty": "oct",
"k": "GawgguFyGrWKav7AX4VKUg",
"alg": "HS256",
"kid": "4c3d..."
}
A JWK Set (or JWKS) is simply a JSON document with a keys array — typically published at /.well-known/jwks.json by OAuth/OIDC providers.
Defined in RFC 7517. → JWK in unjwt.
How they fit together
┌─────────────────────────────┐
│ JWT │ ← the portable string
└──────────────┬──────────────┘
│
┌────────────┴────────────┐
▼ ▼
JWS JWE
(signed) (encrypted)
│ │
└────────────┬────────────┘
▼
JWK
(the key used to produce or consume the above)
- A JWT is either a JWS or a JWE.
- Both use a JWK as the key (or a
CryptoKey/ password / raw bytes, which unjwt normalizes internally).
The claims you'll actually see
Inside a JWT payload, certain claim names are standardized. unjwt validates the ones that affect security automatically:
| Claim | Meaning | Validated by verify() / decrypt() |
|---|---|---|
iss | Issuer | Only if options.issuer is set |
sub | Subject | Only if options.subject is set |
aud | Audience | Only if options.audience is set |
exp | Expires at (seconds) | Always — rejects expired tokens |
nbf | Not before (seconds) | Always — rejects tokens used before the specified time |
iat | Issued at (seconds) | Type-checked; used with options.maxTokenAge |
jti | JWT ID | Not validated, but the H3 session adapters use it as a session.id |
typ | Type header (e.g. JWT) | Only if options.typ is set |
Full definitions in RFC 7519 §4.1.
What's an "algorithm"?
A JWS or JWE header always carries an alg field naming the algorithm the token was produced with. unjwt reads it back to choose the preferred verification path. The algorithms split into two families:
- Signing algorithms (
algin a JWS): HMAC (HS*), RSA (RS*,PS*), ECDSA (ES*), EdDSA (Ed25519). See JWS algorithms. - Key-management algorithms (
algin a JWE): how the content key is delivered. Direct (dir), AES Key Wrap, RSA-OAEP, PBES2, ECDH-ES. See JWE algorithms.
Plus one more for JWE only:
- Content-encryption algorithms (
encin a JWE): AES-GCM or AES-CBC+HMAC-SHA2 — the cipher that encrypts the actual payload.
When you pass a JWK that has an alg field set (which generateJWK() always does), unjwt uses it automatically. You only need to specify alg/enc explicitly when the key carries no such metadata (raw bytes, untyped JWKs, CryptoKey objects without algorithm hints).
Full algorithm registry: RFC 7518.
Keys: representations and secrecy
Two ideas trip up newcomers more than anything else in the Web Crypto world. Both are worth getting straight early.
A key has two representations
| Representation | What it is | Where the key material lives |
|---|---|---|
| JWK | A plain JSON object ({ kty, k, ... }) | In the object — k (symmetric) or d (private) is the secret, in plain text |
| CryptoKey | An opaque handle from the Web Crypto API | Inside the runtime — your code never sees the bytes |
A JWK is portable and inspectable: easy to store, log, or send — which also means easy to leak. A CryptoKey is a reference the runtime signs and decrypts on your behalf; the raw bytes stay locked away. unjwt accepts both everywhere and normalizes internally, so you choose based on what you need: JWKs for portability, CryptoKeys for keeping material out of reach.
Public, private, and secret are three things, not two
asymmetric pair symmetric key
┌─────────────────────┐ ┌───────────────┐
│ public (n,e/x,y) │ ← shareable │ secret (k) │ ← NEVER shareable
│ private (d) │ ← secret │ │
└─────────────────────┘ └───────────────┘
- A public key (asymmetric, no
d) is meant to be published — it only verifies or encrypts. - A private key (asymmetric, has
d) must stay secret — it signs or decrypts. - A secret key (symmetric,
kty: "oct", hask) is the whole secret on its own — used forHS*signing and AES — and is never safe to share.
The catch: "secret" keys are not "private" keys in the d sense. So isPrivateJWK(key) returns false for a symmetric key — and !isPrivateJWK(key) is therefore not a "safe to publish" test, because it passes every oct key. To filter keys you can safely hand out, use isPublicJWK(key) (true only for asymmetric public keys); treat anything isSymmetricJWK(key) reports as a secret. → Type guards
Extractability protects long-lived keys
When you import or generate a CryptoKey, an extractable flag decides whether its raw bytes can ever be read back out (via export to JWK/PEM). With extractable: false, the runtime will still sign and decrypt with the key, but its material can never be exfiltrated — even by code running in the same process.
For long-lived asymmetric private keys that should never leave the server, prefer a non-extractable CryptoKey. Note that producing a JWK inherently requires extractable material (the bytes have to be readable to serialize them), so the two goals are mutually exclusive by design. → Importing & exporting
Ready?
- If you want to start writing code: Quickstart →
- If you want to understand the signed path: JWS →
- If you want the encrypted path: JWE →