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.

"JWT" specifically refers to tokens whose payload is a JSON object of claims (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.

JWS or JWE? Signed is the default choice. Encrypt only when you have a concrete reason — encrypted tokens can't be inspected in logs, shown in DevTools, or debugged by the frontend team. You can also nest a JWS inside a JWE if you want to make sure the sender is trusted and the payload is confidential, but that's a more complex workflow.

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:

ClaimMeaningValidated by verify() / decrypt()
issIssuerOnly if options.issuer is set
subSubjectOnly if options.subject is set
audAudienceOnly if options.audience is set
expExpires at (seconds)Always — rejects expired tokens
nbfNot before (seconds)Always — rejects tokens used before the specified time
iatIssued at (seconds)Type-checked; used with options.maxTokenAge
jtiJWT IDNot validated, but the H3 session adapters use it as a session.id
typType 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 (alg in a JWS): HMAC (HS*), RSA (RS*, PS*), ECDSA (ES*), EdDSA (Ed25519). See JWS algorithms.
  • Key-management algorithms (alg in 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 (enc in 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

RepresentationWhat it isWhere the key material lives
JWKA plain JSON object ({ kty, k, ... })In the objectk (symmetric) or d (private) is the secret, in plain text
CryptoKeyAn opaque handle from the Web Crypto APIInside 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", has k) is the whole secret on its own — used for HS* 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?