A collection of low-level JWT (RFC 7519) utilities using the Web Crypto API. Supports:
- JWS (JSON Web Signature, RFC 7515): sign and verify tokens using HMAC, RSA (RSASSA-PKCS1-v1_5 & RSA-PSS), and ECDSA algorithms.
- JWE (JSON Web Encryption, RFC 7516): encrypt and decrypt data using various key management algorithms (AES Key Wrap, AES-GCM Key Wrap, RSA-OAEP, PBES2, ECDH-ES) and content encryption algorithms (AES-GCM, AES-CBC HMAC-SHA2).
- JWK (JSON Web Key, RFC 7517): generate, import, export, wrap, and unwrap keys in JWK format or as
CryptoKey
objects.
Warning
Please do note that some algorithms are not fully working out-of-the-box yet, such as:
- ECDH-ES algorithms with AES Key Wrap (e.g., ECDH-ES+A128KW)
- RSA algorithms in combination with some bigger CBC encodings (for example RSA-OAEP-256 with A256CBC-HS512)
For these algorithms, you should be able to provide your own keys. Although I do plan to study and simplify this.
Install the package:
# โจ Auto-detect (supports npm, yarn, pnpm, deno and bun)
npx nypm install unjwt
Import:
ESM (Node.js, Bun, Deno)
import { jws, jwe, jwk } from "unjwt";
// JWS functions
import { sign, verify } from "unjwt/jws";
// JWE functions
import { encrypt, decrypt } from "unjwt/jwe";
// JWK functions
import {
generateKey,
importKey,
exportKey,
wrapKey,
unwrapKey,
getJWKFromSet,
importJWKFromPEM,
exportJWKToPEM,
deriveKeyFromPassword,
} from "unjwt/jwk";
// Utility functions
import {
isJWK,
isJWKSet,
isCryptoKey,
isCryptoKeyPair,
base64UrlEncode,
base64UrlDecode,
randomBytes,
} from "unjwt/utils";
CDN (Deno, Bun and Browsers)
import { jws, jwe, jwk } from "https://esm.sh/unjwt";
// JWS functions
import { sign, verify } from "https://esm.sh/unjwt/jws";
// JWE functions
import { encrypt, decrypt } from "https://esm.sh/unjwt/jwe";
// JWK functions
import {
generateKey,
importKey,
exportKey,
wrapKey,
unwrapKey,
getJWKFromSet,
importJWKFromPEM,
exportJWKToPEM,
deriveKeyFromPassword,
} from "https://esm.sh/unjwt/jwk";
// Utility functions
import {
isJWK,
isJWKSet,
isCryptoKey,
isCryptoKeyPair,
base64UrlEncode,
base64UrlDecode,
randomBytes,
} from "https://esm.sh/unjwt/utils";
JWS (JSON Web Signature, RFC 7515)
Functions to sign and verify data according to the JWS specification.
Creates a JWS token.
payload
: The data to sign (string
,Uint8Array
, or a JSON-serializableobject
).key
: The signing key (CryptoKey
,JWK
, orUint8Array
for symmetric keys).options
:alg
: (Required) The JWS algorithm (e.g.,"HS256"
,"RS256"
,"ES256"
,"PS256"
).protectedHeader
: An object for additional JWS Protected Header parameters (e.g.,kid
,typ
,cty
,crit
,b64
). Ifpayload
is an object andtyp
is not set, it defaults to"JWT"
. Theb64
parameter (RFC7797 section-3) controls payload encoding (defaults totrue
, meaning Base64URL encoded).expiresIn
: Sets an expiration time in seconds (e.g.,3600
for 1 hour). Ifiat
is missing it will be set to the current time. Ifexp
is missing it will be set toiat + expiresIn
. This is only applied ifpayload
is a JWT.currentDate
: The current date for computingexpiresIn
option. Defaults tonew Date()
.
Returns a Promise<string>
resolving to the JWS token in Compact Serialization format.
Example (HS256 with string secret):
import { sign } from "unjwt/jws";
const payload = { message: "My important data" }; // Object payload
const secret = "supersecretkey"; // String secret, will be imported (length depends on choosen alg)
const token = await sign(payload, secret, { alg: "HS256" });
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Example (RS256 with CryptoKey):
import { sign } from "unjwt/jws";
import { generateKey } from "unjwt/jwk";
const payload = { userId: 123, permissions: ["read"] };
const { privateKey } = await generateKey("RS256"); // Generates a CryptoKeyPair
const token = await sign(payload, privateKey, { alg: "RS256" });
console.log(token);
// eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Verifies a JWS token.
jws
: The JWS token string.key
: The verification key (CryptoKey
,JWK
,JWKSet
,Uint8Array
, or aKeyLookupFunction
). AKeyLookupFunction
has the signature(header: JWSProtectedHeader, jws: string) => Promise<CryptoKey | JWK | JWKSet | Uint8Array> | CryptoKey | JWK | JWKSet | Uint8Array
.options
(optional):algorithms
: An array of allowed JWSalg
values. If not provided, thealg
from the JWS header is used.critical
: An array of JWS header parameter names that the application understands and processes.
Returns a Promise<JWSVerifyResult<T>>
which is an object { payload: T, protectedHeader: JWSProtectedHeader }
.
The payload
type T
can be JWTClaims
(object), string
, or Uint8Array
depending on the JWS content and headers.
Example (HS256):
import { verify } from "unjwt/jws";
const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."; // From HS256 sign example
const secret = "supersecretkey";
const { payload, protectedHeader } = await verify(token, secret);
console.log(payload); // { message: "My important data" }
console.log(protectedHeader); // { alg: "HS256", typ: "JWT" }
Example (RS256 with key lookup):
import { verify } from "unjwt/jws";
import { generateKey } from "unjwt/jwk";
const token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."; // From RS256 sign example
// Example: using a key lookup function
const { publicKey: rsaPublicKey } = await generateKey("RS256"); // For example purposes, assume publicKey is stored/fetched during lookup
const keyLookup = async (header) => {
if (header.alg === "RS256" /* && header.kid === 'expected-kid' */) {
return rsaPublicKey;
}
throw new Error("Unsupported algorithm or key not found");
};
const { payload } = await verify(token, keyLookup, { algorithms: ["RS256"] });
console.log(payload); // { userId: 123, permissions: ["read"] }
JWE (JSON Web Encryption, RFC 7516)
Functions to encrypt and decrypt data according to the JWE specification.
Encrypts data to produce a JWE token.
payload
: The data to encrypt (string
,Uint8Array
, or a JSON-serializableobject
).key
: The Key Encryption Key (KEK) or password (CryptoKey
,JWK
,string
, orUint8Array
).options
:alg
: (Required) The JWE Key Management algorithm (e.g.,"A128KW"
,"RSA-OAEP-256"
,"PBES2-HS256+A128KW"
,"ECDH-ES+A128KW"
), defaults depends on the key provided.enc
: (Required) The JWE Content Encryption algorithm (e.g.,"A128GCM"
,"A256CBC-HS512"
), defaults depends on the key provided.protectedHeader
: An object for JWE Protected Header parameters (e.g.,kid
,typ
,cty
,crit
,apu
,apv
,p2s
,p2c
). Ifpayload
is an object andtyp
is not set, it defaults to"JWT"
.cek
: (Optional) Provide your own Content Encryption Key (CryptoKey
orUint8Array
).contentEncryptionIV
: (Optional) Provide your own Initialization Vector for content encryption (Uint8Array
).- Other algorithm-specific options like
p2s
,p2c
(for PBES2),keyManagementIV
,ecdhPartyUInfo
,ecdhPartyVInfo
.
Returns a Promise<string>
resolving to the JWE token in Compact Serialization format.
Example (PBES2 password-based encryption):
import { encrypt } from "unjwt/jwe";
const plaintext = "Secret message for password protection";
const password = "myVeryStrongPassword123!";
// Fallback to PBES2-HS256+A128KW and A128GCM if no `alg` and `end` are provided
const jweToken = await encrypt(plaintext, password);
console.log(jweToken);
// JWE token string...
Example (A128KW with A128GCM):
import { encrypt } from "unjwt/jwe";
import { generateKey } from "unjwt/jwk";
const payload = { data: "sensitive information" };
const kek = await generateKey("A128KW"); // AES Key Wrap key
const jweToken = await encrypt(payload, kek, {
alg: "A128KW",
enc: "A128GCM",
protectedHeader: { kid: "aes-key-1" },
});
console.log(jweToken);
// eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwia2lkIjoiYWVzLWtleS0xIn0...
Decrypts a JWE token.
jwe
: The JWE token string.key
: The Key Decryption Key (KDK) or password (CryptoKey
,JWK
,string
,Uint8Array
, or aJWEKeyLookupFunction
). AJWEKeyLookupFunction
has the signature(header: JWEHeaderParameters) => Promise<CryptoKey | JWK | string | Uint8Array> | CryptoKey | JWK | string | Uint8Array
.options
(optional):algorithms
: Array of allowed JWE Key Managementalg
values.encryptionAlgorithms
: Array of allowed JWE Content Encryptionenc
values.critical
: Array of JWE header parameter names that the application understands.unwrappedKeyAlgorithm
: (ForunwrapKey
internally) Algorithm details for the CEK after unwrapping.keyUsage
: (ForunwrapKey
internally) Intended usages for the unwrapped CEK.
Returns a Promise<JWEDecryptResult<T>>
which is an object { payload: T, protectedHeader: JWEHeaderParameters, cek: Uint8Array, aad: Uint8Array }
.
The payload
type T
can be JWTClaims
(object) or string
.
Example (PBES2 password-based decryption):
import { decrypt } from "unjwt/jwe";
// const jweToken = ...; // From PBES2 encrypt example
// const password = "myVeryStrongPassword123!";
const { payload } = await decrypt(jweToken, password);
Example (A128KW with A128GCM):
import { decrypt } from "unjwt/jwe";
// const jweToken = "eyJhbGciOiJBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwia2lkIjoiYWVzLWtleS0xIn0...";
// const kek = ...; // The same AES Key Wrap key used for encryption
async function decryptData(jweToken: string, kek: CryptoKey) {
try {
const { payload, protectedHeader, cek } = await decrypt(jweToken, kek, {
algorithms: ["A128KW"],
encryptionAlgorithms: ["A128GCM"],
});
console.log("Decrypted Plaintext:", payload);
console.log("Protected Header:", protectedHeader);
// console.log("CEK (Content Encryption Key):", cek);
} catch (error) {
console.error("Decryption failed:", error);
}
}
JWK (JSON Web Key, RFC 7517)
Utilities for working with JSON Web Keys.
Generates a cryptographic key.
alg
: The JWA algorithm identifier for the key to be generated (e.g.,"HS256"
,"RS256"
,"ES256"
,"A128KW"
,"A128GCM"
,"A128CBC-HS256"
).options
(optional):toJWK
: Iftrue
, returns the key(s) in JWK format. Otherwise, returnsCryptoKey
(s) orUint8Array
(for composite keys like AES-CBC-HS*). Defaultfalse
.extractable
: Boolean, whether the generatedCryptoKey
can be exported. Defaulttrue
.keyUsage
: Array ofKeyUsage
strings. Defaults are algorithm-specific.modulusLength
: For RSA keys (e.g.,2048
,4096
). Default2048
.publicExponent
: For RSA keys. Defaultnew Uint8Array([0x01, 0x00, 0x01])
.
Returns a Promise
resolving to CryptoKey
, CryptoKeyPair
, Uint8Array
(for composite keys), JWK
, or { privateKey: JWK, publicKey: JWK }
depending on alg
and options.toJWK
.
Examples:
import { generateKey } from "unjwt/jwk";
// Generate an HS256 CryptoKey
const hmacKey = await generateKey("HS256");
console.log(hmacKey); // CryptoKey
// Generate an RS256 CryptoKeyPair
const rsaKeyPair = await generateKey("RS256", { modulusLength: 2048 });
console.log(rsaKeyPair.publicKey); // CryptoKey
console.log(rsaKeyPair.privateKey); // CryptoKey
// Generate an ES384 key pair as JWKs
const ecJwks = await generateKey("ES384", { toJWK: true });
console.log(ecJwks.publicKey); // JWK
console.log(ecJwks.privateKey); // JWK
// Generate a composite key for A128CBC-HS256 as Uint8Array
const aesCbcHsKeyBytes = await generateKey("A128CBC-HS256");
console.log(aesCbcHsKeyBytes); // Uint8Array (32 bytes: 16 for AES, 16 for HMAC)
// Generate an A256GCM key as a JWK
const aesGcmJwk = await generateKey("A256GCM", { toJWK: true });
console.log(aesGcmJwk); // JWK
Derives a key from a password using PBKDF2 for PBES2 algorithms.
password
: The password (string
orUint8Array
).alg
: The PBES2 algorithm (e.g.,"PBES2-HS256+A128KW"
).options
:salt
: The salt (Uint8Array
, at least 8 octets).iterations
: The iteration count (positive integer).toJWK
: Iftrue
, returns aJWK_oct
. Otherwise,CryptoKey
. Defaultfalse
.extractable
: Boolean forCryptoKey
. Defaultfalse
unlesstoJWK
is true.keyUsage
: ForCryptoKey
. Default["wrapKey", "unwrapKey"]
.
Returns a Promise
resolving to CryptoKey
or JWK_oct
.
Example:
import { deriveKeyFromPassword } from "unjwt/jwk";
import { randomBytes, textEncoder } from "unjwt/utils";
const password = "mySecretPassword";
const salt = randomBytes(16);
const iterations = 4096;
const derivedKey = await deriveKeyFromPassword(password, "PBES2-HS384+A192KW", {
salt,
iterations,
});
console.log(derivedKey); // CryptoKey for AES-KW (192-bit)
const derivedJwk = await deriveKeyFromPassword(password, "PBES2-HS512+A256KW", {
salt,
iterations,
toJWK: true,
});
console.log(derivedJwk); // JWK_oct { kty: "oct", k: "...", alg: "A256KW" }
Imports a key from various formats. This is a flexible wrapper.
keyMaterial
: The key to import. Can be:CryptoKey
: Returned directly.Uint8Array
: Returned directly (treated as raw symmetric key bytes).string
: Encoded toUint8Array
and returned.JWK_oct
(symmetric JWK withk
property): Thek
value is Base64URL decoded and returned asUint8Array
.- Other
JWK
types (asymmetric): Imported into aCryptoKey
.
alg
(optional): The JWA algorithm string. Required when importing asymmetric JWKs (e.g., RSA, EC) to provide context forcrypto.subtle.importKey
.
Returns a Promise
resolving to CryptoKey
or Uint8Array
.
Examples:
import { importKey } from "unjwt/jwk";
import { textEncoder, base64UrlDecode } from "unjwt/utils";
// Import raw symmetric key bytes
const rawBytes = textEncoder.encode("a-32-byte-long-secret-key-123"); // 32 bytes for AES-256 or HS256
const symmetricKeyBytes = await importKey(rawBytes);
console.log(symmetricKeyBytes); // Uint8Array
// Import a symmetric JWK (kty: "oct")
const octJwk = {
kty: "oct",
k: "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr0", // Example key
};
const importedOctBytes = await importKey(octJwk); // Returns Uint8Array
console.log(importedOctBytes); // Uint8Array (decoded from k)
// Import an RSA Public Key JWK
const rsaPublicJwk = {
kty: "RSA",
n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3ok92YEnjsADC4Ue87zwRzH2J-TCwlcQrY3E9gGZJZL2g_2_5QjLhL0gR0xYj04_N4M",
e: "AQAB",
alg: "RS256",
kid: "rsa-pub-1",
};
const rsaPublicKey = await importKey(rsaPublicJwk, "RS256"); // 'alg' is crucial here
console.log(rsaPublicKey); // CryptoKey
Exports a CryptoKey
to JWK format.
key
: TheCryptoKey
to export (must beextractable
).jwk
(optional): A partialJWK
object to merge with the exported properties (e.g., to addkid
,use
, or overridealg
).
Returns a Promise<JWK>
.
Example:
import { generateKey, exportKey } from "unjwt/jwk";
const { publicKey } = await generateKey("ES256"); // Generates an extractable CryptoKey
const jwk = await exportKey(publicKey, { kid: "ec-key-001", use: "sig" });
console.log(jwk);
// {
// kty: 'EC',
// crv: 'P-256',
// x: '...',
// y: '...',
// ext: true,
// key_ops: [ 'verify' ], // or as per generation
// kid: 'ec-key-001',
// use: 'sig'
// }
Wraps a Content Encryption Key (CEK).
alg
: The JWA key management algorithm (e.g.,"A128KW"
,"RSA-OAEP"
).keyToWrap
: The CEK to wrap (CryptoKey
orUint8Array
).wrappingKey
: The Key Encryption Key (KEK) (CryptoKey
,JWK
, or passwordstring
/Uint8Array
for PBES2).options
(optional): Algorithm-specific options (e.g.,p2s
,p2c
for PBES2;iv
for AES-GCMKW).
Returns a Promise<WrapKeyResult>
containing encryptedKey
and other parameters like iv
, tag
, epk
, p2s
, p2c
as needed by the algorithm.
Example (AES Key Wrap):
import { wrapKey, generateKey } from "unjwt/jwk";
import { randomBytes } from "unjwt/utils";
const cekToWrap = randomBytes(32); // e.g., a 256-bit AES key as Uint8Array
const kek = await generateKey("A128KW"); // 128-bit AES Key Wrap key
const { encryptedKey } = await wrapKey("A128KW", cekToWrap, kek);
console.log("Wrapped CEK:", encryptedKey); // Uint8Array
Unwraps a Content Encryption Key (CEK).
alg
: The JWA key management algorithm.wrappedKey
: The encrypted CEK (Uint8Array
).unwrappingKey
: The Key Decryption Key (KDK).options
(optional):returnAs
: Iffalse
, returnsUint8Array
. Iftrue
(default) or undefined, returnsCryptoKey
.unwrappedKeyAlgorithm
:AlgorithmIdentifier
for the imported CEK ifreturnAs
istrue
.keyUsage
:KeyUsage[]
for the imported CEK ifreturnAs
istrue
.extractable
: Boolean for the imported CEK.- Other algorithm-specific options (e.g.,
p2s
,p2c
,iv
,tag
,epk
).
Returns a Promise
resolving to the unwrapped CEK as CryptoKey
or Uint8Array
.
Example (AES Key Unwrap):
import { unwrapKey, generateKey } from "unjwt/jwk";
// const encryptedKey = ...; // From wrapKey example
// const kdk = ...; // Same KEK used for wrapping
async function unwrapMyKey(encryptedKey: Uint8Array, kdk: CryptoKey) {
const unwrappedCekBytes = await unwrapKey("A128KW", encryptedKey, kdk, {
returnAs: false, // Get raw bytes
});
console.log("Unwrapped CEK (bytes):", unwrappedCekBytes); // Uint8Array
const unwrappedCekCryptoKey = await unwrapKey("A128KW", encryptedKey, kdk, {
returnAs: true, // Get CryptoKey
unwrappedKeyAlgorithm: { name: "AES-GCM", length: 256 }, // Specify CEK's intended alg
keyUsage: ["encrypt", "decrypt"],
});
console.log("Unwrapped CEK (CryptoKey):", unwrappedCekCryptoKey); // CryptoKey
}
Imports a key from a PEM-encoded string and converts it to a JWK.
pem
: The PEM-encoded string (including-----BEGIN ...-----
and-----END ...-----
markers).pemType
: The type of PEM encoding:"pkcs8"
: For private keys in PKCS#8 format."spki"
: For public keys in SPKI format."x509"
: For X.509 certificates (extracts the public key).
alg
: The JWA algorithm identifier (e.g.,"RS256"
,"ES256"
). This is crucial forcrypto.subtle.importKey
to understand the key's intended algorithm and for setting the'alg'
field in the resulting JWK.importOptions
(optional): Options for the underlyingcrypto.subtle.importKey
call:extractable
: Boolean, whether the importedCryptoKey
should be extractable. Defaults totrue
.keyUsage
: Array ofKeyUsage
strings for the importedCryptoKey
.
jwkExtras
(optional): An object containing additional properties to merge into the resulting JWK (e.g.,"kid"
,"use"
).
Returns a Promise<JWK>
resolving to the imported key as a JWK.
Example:
import { importJWKFromPEM } from "unjwt/jwk";
const rsaPublicJwk = await importJWKFromPEM(
provess.env.RSA_PEM_SPKI, // PEM string
"spki",
"RS256",
{ extractable: false },
{ kid: "my-rsa-key" }, // Additional properties to add to the JWK
);
console.log(rsaPublicJwk);
// {
// kty: 'RSA',
// alg: 'RS256',
// kid: 'my-rsa-key',
// n: '...',
// e: 'AQAB',
// ext: false,
// key_ops: [ 'verify' ]
// }
Exports a JWK to a PEM-encoded string.
- jwk: The JWK to export.
- pemFormat: The desired PEM format:
- "pkcs8": For private keys in PKCS#8 format.
- "spki": For public keys in SPKI format.
- algForCryptoKeyImport (optional): If the JWK does not have an 'alg' property, this algorithm hint is required to correctly convert it to a CryptoKey first. This is only needed if the JWK lacks an alg property.
Returns a Promise<string>
resolving to the PEM-encoded key string.
Example:
import { exportJWKToPEM } from "unjwt/jwk";
import { rsaJWK } from "./keys"; // Assuming you have JWKs in keys.ts
const rsaPrivatePem = await exportJWKToPEM(rsaJWK.private, "pkcs8");
console.log(rsaPrivatePem);
// -----BEGIN PRIVATE KEY-----
// MII...
// -----END PRIVATE KEY-----
const rsaPublicSpki = await exportJWKToPEM(
rsaJWK.public,
"spki",
"RS256", // this is required if `rsaJWK.public.alg` is undefined
);
console.log(rsaPublicSpki);
// -----BEGIN PUBLIC KEY-----
// MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQE...
// -----END PUBLIC KEY-----
unjwt/utils
exports several helpful functions:
base64UrlEncode(data: Uint8Array | string): string
base64UrlDecode(str?: string, toString?: boolean): Uint8Array | string
(Decodes to string by default, orUint8Array
iftoString
isfalse
)randomBytes(length: number): Uint8Array
textEncoder: TextEncoder
textDecoder: TextDecoder
- Type guards:
isJWK(key)
,isCryptoKey(key)
,isCryptoKeyPair(keyPair)
local development
Originally developed by Johann Schopplich. Heavily inspired by Filip Skokan's work.
Published under the MIT license.
Made by community ๐
๐ค auto updated with automd