mrkeyoor.com_
Sun 20 Sept 05:55 UTC
npmSecurityupdated 20 Sept 2026

jose review

Our August 22 install of jose 6.2.10 finished in 0.4 seconds, left one package and 1 MB on disk, and produced no npm audit findings. jose implements the JOSE standards in JavaScript: JWT claim validation, JWS signing and verification, JWE encryption, JWK conversion, and local or remote JWKS key selection. Its Web Crypto based API runs in Node, browsers, Workers, Deno, Bun, and other compatible runtimes. Current release 6.2.12 keeps the v6 public surface and changes the machinery underneath it. Its release notes name fewer copies during AES-GCM work, one encoding pass for single-signature JWS input, shared-header normalization for General JWE, and deduplication of pending JWKS key imports. It also shortens the generated API guidance. No new public operation was announced in this release.

95.8Mdownloads / wk
Verdict

Our jose 6.2.10 install took 0.4 seconds, occupied 1 MB, pulled zero dependencies, and returned zero audit findings, so its package cost is easy to justify when an application needs JOSE primitives. Install current 6.2.12 for JWT, JWS, JWE, and JWKS work across Web Crypto runtimes; choose an authentication framework when the missing piece is session or authorization policy.

We installed it

Lab card: what happened when we installed joseScreenshot of jose documentation
Install✓ · 0.4s1 package on disk · 1 MB
ImportESM import works · require() works · ESM package with exports map
Browser18.8 KBgzipped (59.6 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does jose install cleanly?

Yes. In a fresh container with an empty cache, npm install jose finished in 0.4s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does jose add to a browser bundle?

18.8 KB gzipped (59.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does jose work with both ESM and CommonJS?

Yes. Both import 'jose' and require('jose') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does jose include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

jose or jsonwebtoken: which should you use?

jsonwebtoken: Pick it for a CommonJS Node service whose scope is ordinary JWT signing and verification. Our jose 6.2.10 install took 0.4 seconds, occupied 1 MB, pulled zero dependencies, and returned zero audit findings, so its package cost is easy to justify when an application needs JOSE primitives.

When should you not use jose?

You need login flows, sessions, revocation, replay prevention, or authorization rules. The project's security policy assigns all of those jobs to the application.

API stability4/5jose 6.2.12 retains the v6 entry points for JWT, JWS, JWE, JWK, JWKS, key import, and typed errors; its listed changes are documentation, refactoring, and performance work. The major-version boundary still matters because v6 is ESM, and CommonJS access depends on a runtime that can require ESM. Patch releases may also tighten input checks without renaming an API, so malformed tokens or keys can stop passing after an update.
Docs4/5The v6 README links each operation to generated API pages, separates JWT work from raw JWS and JWE serializations, and points to algorithm support for six named runtime families. The reference spells out JWKS cache behavior, error classes, key formats, and verification options. Its security policy is unusually direct about what callers still own, including authorization, replay, revocation, input limits, trust anchors, and certificate validation.
Maintenance5/5jose 6.2.12 was published on September 5, 2026, and GitHub records the latest push on September 11. The repository has 7,791 stars, is not archived, and both the true open-issue search and GitHub's combined issues-and-PR counter returned zero. The security policy currently supports v6.x, while the latest release notes identify specific JWKS, JWS, JWE, AES-GCM, and documentation changes.
Ecosystem5/5npm recorded 97,835,668 jose downloads from September 5 through September 11, 2026, and GitHub reports 7,791 stars. The README names Node, browsers, Cloudflare Workers, Deno, Bun, and Electron, with algorithm support tracked per runtime. One package covers compact and JSON JWS, compact and JSON JWE, JWT claims, remote and local JWKS resolution, PEM and JWK conversion, and RFC thumbprints; our measured install added zero dependencies.

Use it if

  • A v6 verifier must share code between Node and a Web Crypto based edge or browser runtime.
  • Your issuer publishes rotating signing keys and you need a remote JWKS resolver with cache age, cooldown, timeout, and reload controls.
  • The job includes JWE, JSON JWS serializations, PEM or JWK conversion, or thumbprints in addition to compact signed JWTs.
  • Zero direct dependencies and bundled TypeScript declarations matter at the token verification boundary.
Skip it if

Setup reality

Our test method used an unprivileged Node 22 Bookworm container with 3 CPUs, 8 GB of RAM, and no cache. We installed jose 6.2.10 there on August 22, 2026: npm finished in 0.4 seconds, one installed package occupied 1 MB, and audit reported 0 known vulnerabilities. The package had no direct or peer dependencies, 592 KB unpacked, an MIT license, bundled TypeScript declarations, and an exports map. ESM import and require() both loaded. An all-exports browser build measured 59.6 KB minified and 18.8 KB gzipped.

Zero dependencies does not remove key setup. Encode an HMAC secret into bytes, import private PEM with importPKCS8, public PEM with importSPKI, and a certificate's public key with importX509. Each PEM import needs its JOSE algorithm. Generated private keys are non-extractable unless you request extractable: true, so exporting a default generated pair can fail by design. jose never stores or rotates private material for you.

jwtVerify applies the policy you pass. Set accepted algorithms, issuer, audience, token type, required claims, and maximum age where the protocol calls for them. decodeJwt and decodeProtectedHeader only parse attacker-controlled input. importX509 extracts a public key; the security policy says it does not validate the certificate chain, dates, revocation, names, or issuer binding. Header values such as jku, x5u, and embedded jwk need an application-owned trust rule.

Create one createRemoteJWKSet resolver and reuse it so its in-memory key set, cooldown, and active reload survive a request. Version 6.2.12 deduplicates pending JWKS key imports, but it does not supply a shared cache across processes. Use a trusted HTTPS URL and protect any external cache writer because cached keys become verification keys. A remote resolver supports public signature keys only and requires exactly one match. Test the exact algorithm on every runtime; the package loading successfully does not prove that Web Crypto implements that algorithm.

Patterns

Verify an HS256 token with fixed policy verify-hmac-jwt

import { jwtVerify } from 'jose';

const value = process.env.JWT_SECRET;
if (!value) throw new Error('JWT_SECRET is missing');
const secret = new TextEncoder().encode(value);

const { payload, protectedHeader } = await jwtVerify(token, secret, {
  algorithms: ['HS256'],
  issuer: 'https://issuer.example',
  audience: 'orders-api',
  requiredClaims: ['exp'],
});

`jwtVerify` checks the signature and the configured claims in one call. Keep the allowed algorithm in application configuration instead of reading it from the token.

Sign a JWT that expires in 15 minutes issue-short-jwt

import { SignJWT } from 'jose';

const token = await new SignJWT({ role: 'editor' })
  .setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
  .setSubject('user_123')
  .setIssuer('https://issuer.example')
  .setAudience('orders-api')
  .setIssuedAt()
  .setExpirationTime('15m')
  .sign(secret);

`SignJWT` requires an `alg` in the protected header. The `15m` duration is calculated from the current time when the token is built.

Resolve a rotating public signing key verify-remote-jwks

import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(
  new URL('https://id.example/.well-known/jwks.json'),
);

export async function verifyAccessToken(token) {
  return jwtVerify(token, JWKS, {
    algorithms: ['RS256'],
    issuer: 'https://id.example',
    audience: 'orders-api',
  });
}

A remote resolver caches public signature keys and refetches after a missing `kid` only when its cooldown permits. Define it outside the request function to retain that state.

Verify against an in-memory key set verify-local-jwks

import { createLocalJWKSet, jwtVerify } from 'jose';

const resolveKey = createLocalJWKSet({ keys: publicKeys });
const result = await jwtVerify(token, resolveKey, {
  algorithms: ['ES256'],
  issuer: expectedIssuer,
  audience: expectedAudience,
});

`createLocalJWKSet` selects by protected-header metadata and requires one matching key. Construct another resolver after replacing the key-set object.

Import separate private and public PEM files import-pem-pair

import { importPKCS8, importSPKI } from 'jose';

const signingKey = await importPKCS8(privatePem, 'RS256');
const verificationKey = await importSPKI(publicPem, 'RS256');

PKCS #8 is the private-key input and SPKI is the public-key input. Both import calls need the JOSE algorithm that will use the key.

Generate an RSA pair that can be exported generate-exportable-keys

import { exportPKCS8, exportSPKI, generateKeyPair } from 'jose';

const { privateKey, publicKey } = await generateKeyPair('RS256', {
  extractable: true,
});
const privatePem = await exportPKCS8(privateKey);
const publicPem = await exportSPKI(publicKey);

Generated private keys default to non-extractable. Request extraction only when the application must persist or transfer the generated material.

Read a token before deciding how to route it inspect-untrusted-jwt

import { decodeJwt, decodeProtectedHeader } from 'jose';

const header = decodeProtectedHeader(token);
const claims = decodeJwt(token);
console.log({ alg: header.alg, issuer: claims.iss });

`decodeJwt` and `decodeProtectedHeader` perform no signature or claim validation. Treat every returned value as attacker-controlled until verification succeeds.

Encrypt and validate private JWT claims encrypt-jwt

import { EncryptJWT, generateSecret, jwtDecrypt } from 'jose';

const key = await generateSecret('A256GCM');
const token = await new EncryptJWT({ sub: 'user_123' })
  .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
  .setIssuer('https://issuer.example')
  .setAudience('orders-api')
  .setIssuedAt()
  .setExpirationTime('10m')
  .encrypt(key);

const { payload } = await jwtDecrypt(token, key, {
  keyManagementAlgorithms: ['dir'],
  contentEncryptionAlgorithms: ['A256GCM'],
  issuer: 'https://issuer.example',
  audience: 'orders-api',
});

`EncryptJWT` creates a compact JWE whose claims are hidden from the holder. Pin both `alg` and `enc` choices when decrypting untrusted ciphertext.

Sign bytes without JWT claim rules sign-byte-payload

import { CompactSign } from 'jose';

const body = new TextEncoder().encode('invoice=42');
const signed = await new CompactSign(body)
  .setProtectedHeader({ alg: 'HS256' })
  .sign(secret);

`CompactSign` accepts bytes and produces compact JWS. It does not add or validate JWT claims such as `iss`, `aud`, or `exp`.

Handle expiry separately from signature failure classify-jose-errors

import { errors, jwtVerify } from 'jose';

try {
  return await jwtVerify(token, verificationKey, policy);
} catch (error) {
  if (error instanceof errors.JWTExpired) return refreshSession();
  if (error instanceof errors.JWSSignatureVerificationFailed) return rejectToken();
  throw error;
}

`JWTExpired` also covers a token older than `maxTokenAge`. A bad signature has the separate `JWSSignatureVerificationFailed` class and code.

Allow 30 seconds of skew and cap token age bound-token-age

const result = await jwtVerify(token, verificationKey, {
  algorithms: ['RS256'],
  issuer: expectedIssuer,
  audience: expectedAudience,
  clockTolerance: '30s',
  maxTokenAge: '1h',
});

`clockTolerance` applies to `nbf` and `exp`, plus `iat` when maximum age is checked. `maxTokenAge` requires an `iat` claim.

Give a public JWK a repeatable key ID publish-public-jwk

import { calculateJwkThumbprint, exportJWK } from 'jose';

const jwk = await exportJWK(publicKey);
jwk.kid = await calculateJwkThumbprint(jwk);
jwk.alg = 'RS256';
jwk.use = 'sig';

const jwks = { keys: [jwk] };

`calculateJwkThumbprint` derives a stable identifier from the public JWK members. Publish the public key only; private JWK fields do not belong in a JWKS response.

Alternatives

PackageRegistryPick it when
jsonwebtokennpmPick it for a CommonJS Node service whose scope is ordinary JWT signing and verification.
fast-jwtnpmCompare it in a Node benchmark when JWT throughput, rather than JWE or cross-runtime support, is the reason for changing libraries.
jwt-decodenpmUse it in interface code that reads untrusted claims for display and never makes an access decision from them.
pasetonpmChoose it when both ends can use PASETO and you want protocol versions with a smaller set of cryptographic choices.

More security guides

cryptography · pyjwt · requests-oauthlib · oauthlib · dompurify · jsonwebtoken · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.