mrkeyoor.com_
Thu 06 Aug 01:01 UTC
npmSecurityupdated 05 Aug 2026

jose

jose is the JavaScript implementation of the JOSE family of specs: JWT signing and verification, JWS signatures, JWE encryption, JWK keys, and remote JWKS fetching. It is built entirely on the WebCrypto API, ships zero dependencies, and runs unchanged in Node.js, browsers, Cloudflare Workers, Deno, and Bun. That runtime spread is the reason it displaced jsonwebtoken as the default for new code: the same verify call works in a Worker at the edge and in a Node server, and the maintainer also builds openid-client and oauth4webapi on top of it.

Verdict

The default JWT library for new JavaScript code in 2026: standards-complete, zero-dependency, and at home in every runtime that matters. Only reach for jsonwebtoken out of legacy inertia, and for jwt-decode when you truly just need to peek at claims.

API stability4/5The core sign/verify surface has barely moved since v4, but the project supports exactly one major at a time (currently v6), so upgrades are mandatory for security fixes, and v5 to v6 moved key types onto WebCrypto CryptoKey.
Docs4/5Every export has a markdown reference page with runnable examples in the repo, and the README maps use cases to functions well; there is no narrative guide site though, so beginners piece flows together from reference pages.
Maintenance5/5Pushed the day before this review with four releases in the previous month, Auth0 sponsorship, and a published security policy; the open issue count reads zero because the maintainer routes discussion aggressively and closes fast.
Ecosystem5/5Around 114M weekly downloads, the foundation under openid-client and oauth4webapi, and the JWT library that edge platforms' own docs reach for in examples.

Use it if

  • You verify or mint JWTs anywhere outside plain Node, such as Cloudflare Workers, Vercel Edge, Deno, or the browser, where jsonwebtoken simply does not run
  • You need to verify tokens against a provider's JWKS endpoint; createRemoteJWKSet handles fetching, caching, and key rotation for you
  • You need more than HS256/RS256: EdDSA, ECDH-ES, JWE encrypted tokens, or JWK thumbprints are all in the box
  • You want zero dependencies in a security-critical path instead of jsonwebtoken's dependency tree
Skip it if

Setup reality

npm install jose and there are no peer dependencies, no config, no native builds. The friction points are elsewhere: it is ESM-first, so old CJS setups need Node's require(esm) support or a bundler; every key must be a CryptoKey or Uint8Array, so string secrets from jsonwebtoken code need new TextEncoder().encode() and PEM keys need an explicit importPKCS8/importSPKI call with the algorithm named; and generated keys are non-extractable by default, which surprises people who try to export them.

Patterns

Verify a JWT with a shared secretverify-jwt-hs256

import { jwtVerify } from 'jose'

const secret = new TextEncoder().encode(process.env.JWT_SECRET)

const { payload, protectedHeader } = await jwtVerify(token, secret, {
  issuer: 'urn:example:issuer',
  audience: 'urn:example:audience',
  algorithms: ['HS256'],
})

Secrets must be Uint8Array, not a string; coming from jsonwebtoken this is the first thing that breaks. Always pin algorithms so a swapped alg header cannot downgrade verification.

Mint a signed JWTsign-jwt

import { SignJWT } from 'jose'

const jwt = await new SignJWT({ sub: 'user_123', role: 'admin' })
  .setProtectedHeader({ alg: 'HS256' })
  .setIssuedAt()
  .setIssuer('urn:example:issuer')
  .setAudience('urn:example:audience')
  .setExpirationTime('2h')
  .sign(secret)

setProtectedHeader is mandatory; forgetting it throws at sign time. Relative times like '2h' are resolved against the current clock.

Verify against a provider's JWKS endpointverify-remote-jwks

import { jwtVerify, createRemoteJWKSet } from 'jose'

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

const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://auth.example.com',
  audience: 'my-api',
})

Create the JWKS set once at module scope, not per request: it caches keys and refetches on unknown kid with a cooldown, which is how key rotation gets handled for free.

Generate and export an asymmetric key pairgenerate-key-pair

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

const { publicKey, privateKey } = await generateKeyPair('RS256', {
  extractable: true,
})

const privatePem = await exportPKCS8(privateKey)
const publicPem = await exportSPKI(publicKey)

Keys are non-extractable CryptoKeys by default in v6; without extractable: true the export calls fail. Leave them non-extractable when you never need to persist them.

Import PEM keys for signing or verificationimport-pem-key

import { importPKCS8, importSPKI, jwtVerify } from 'jose'

const privateKey = await importPKCS8(privatePem, 'RS256')
const publicKey = await importSPKI(publicPem, 'RS256')

const { payload } = await jwtVerify(token, publicKey)

The algorithm argument is required; jose refuses to guess it from the PEM contents. importX509 exists for certificate PEMs and importJWK for JWK objects.

Decode claims without verifyingdecode-without-verify

import { decodeJwt, decodeProtectedHeader } from 'jose'

const claims = decodeJwt(token)
const header = decodeProtectedHeader(token)
console.log(claims.exp, header.alg)

No signature check happens at all. Fine for showing an expiry in the UI or routing by issuer before verification; never make an auth decision from it.

Encrypted JWT (JWE) round tripencrypt-jwt

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

const secret = await generateSecret('A256GCM')

const jwe = await new EncryptJWT({ sub: 'user_123' })
  .setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
  .setIssuedAt()
  .setExpirationTime('10m')
  .encrypt(secret)

const { payload } = await jwtDecrypt(jwe, secret)

Use JWE when the claims themselves are sensitive; a normal signed JWT is readable by anyone who holds it. dir with A256GCM is the simplest shared-secret setup.

Branch on specific verification failureshandle-verify-errors

import { jwtVerify, errors } from 'jose'

try {
  await jwtVerify(token, secret)
} catch (err) {
  if (err instanceof errors.JWTExpired) {
    // ask the client to refresh
  } else if (err instanceof errors.JWSSignatureVerificationFailed) {
    // tampered or wrong key
  } else {
    throw err
  }
}

Every failure is a typed class under errors with a stable code like ERR_JWT_EXPIRED, so you can also switch on err.code instead of instanceof.

Allow clock skew and cap token ageclock-tolerance

const { payload } = await jwtVerify(token, secret, {
  clockTolerance: '30s',
  maxTokenAge: '1h',
})

clockTolerance absorbs skew between the issuer's clock and yours; maxTokenAge rejects tokens whose iat is too old even if exp has not passed, which is useful against replay of long-lived tokens.

Export a public JWK with a thumbprint kidexport-jwk-thumbprint

import { exportJWK, calculateJwkThumbprint } from 'jose'

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

// serve { keys: [jwk] } from your own JWKS endpoint

The RFC 7638 thumbprint gives you a deterministic kid, so verifiers can match keys after rotation without you inventing an id scheme.

Alternatives

PackageRegistryPick it when
jsonwebtokennpmA legacy CommonJS Node codebase already using it and only doing plain HS256/RS256 sign and verify.
fast-jwtnpmNode-only services where token throughput actually shows up in profiles and you can skip cross-runtime support.
jwt-decodenpmBrowser code that only reads claims for display and lets the server do the real verification.
pasetonpmYou control both ends and prefer PASETO's misuse-resistant token design over the JOSE spec family.