Safeguard
Open Source

jose npm: A Security Review and Safe Usage Guide

The jose npm package is a well-regarded library for JWT, JWS, and JWE across JavaScript runtimes. It gives you the right primitives; using them securely still comes down to how you verify tokens.

Yukti Singhal
Platform Engineer
6 min read

The jose npm package is a well-maintained, standards-focused JavaScript library for JSON Web Tokens (JWT), signatures (JWS), and encryption (JWE), and it is a solid choice — but like any crypto library, it is only as safe as the way you call it. Maintained by panva, jose works across Node.js, browsers, Cloudflare Workers, Deno, Bun, and other web-interoperable runtimes, which is a big part of its appeal: one library, consistent behavior everywhere. When people search npm jose, this is almost always the package they mean.

The library gets the hard parts right. The mistakes that lead to token-forgery vulnerabilities are almost never in jose's code; they are in how developers configure verification. This review focuses on that boundary.

What jose gives you

jose covers the full JSON Object Signing and Encryption family: signing and verifying JWS, encrypting and decrypting JWE, working with JSON Web Keys (JWK) and key sets (JWKS), and handling the various serialization formats. Install it with npm i jose; the current major line is 6.x.

A typical verification of a signed JWT against a provider's public key set:

import { jwtVerify, createRemoteJWKSet } from 'jose';

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

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

Notice what that call does beyond checking the signature: it validates the issuer and audience. Those options are not decoration — leaving them out is one of the most common ways token verification goes wrong.

The alg confusion trap

The single most important thing to understand about verifying JWTs is algorithm confusion, and jose is designed to steer you away from it.

A JWT's header declares which algorithm signed it (alg). The dangerous mistake is letting the token dictate how you verify it. Two classic attacks live here:

  1. The none algorithm. A token can claim alg: none, meaning "no signature." A naive verifier that honors the header accepts an unsigned, attacker-forged token.
  2. RS256-to-HS256 confusion. If your server verifies with a public key but a library lets an attacker switch the header to a symmetric algorithm (HS256), the attacker can sign a forged token using your public key as the HMAC secret — because the public key is, by definition, public.

The defense is to never trust the token's alg and instead pin the algorithms you accept. jose supports this directly:

const { payload } = await jwtVerify(token, JWKS, {
  issuer: 'https://idp.example.com',
  audience: 'my-api',
  algorithms: ['RS256'],   // accept only what you expect
});

With algorithms pinned, a token claiming none or HS256 is rejected before verification even begins. Set this explicitly on every verification. It is the highest-leverage line in the whole flow.

Verify the claims, not just the signature

A valid signature only tells you the token was issued by the key holder. It does not tell you the token is meant for you, or that it is still valid. jose validates the registered claims when you pass the options, and you should pass them:

  • issuer — confirm the token came from the identity provider you expect.
  • audience — confirm the token was minted for your service, not a different one that trusts the same issuer.
  • Expiration (exp) and not-before (nbf) are checked automatically, with a configurable clock tolerance for small skew between servers.

Skipping issuer and audience checks is how a token legitimately issued for service A gets replayed against service B. The signature is real; the token was simply never meant for you.

Encryption is not authentication

jose also does JWE, and it is worth being clear about what encryption buys you. A JWE keeps a payload confidential from anyone who cannot decrypt it. It does not, by itself, tell you who created it in the way a signature does. If you need both confidentiality and proof of origin, you sign and then encrypt (nested JWT), and you verify the inner signature after decrypting. Reaching for encryption when what you actually needed was a signature is a conceptual error that jose cannot catch for you.

Keeping the dependency healthy

jose is a focused library with a clean dependency footprint, which is exactly what you want in something sitting on your authentication path. It is actively maintained, and staying current matters because crypto libraries occasionally ship fixes for edge-case parsing or validation issues. Pin your version, review release notes when you upgrade rather than bumping blindly, and let a software composition analysis scan watch for any advisory that affects the version you run. Because jose keeps its own dependency tree small, most of your real risk is in your verification configuration rather than in transitive packages — but the monitoring is cheap and worth having.

A checklist for using jose safely

  1. Always pin algorithms to the exact set you expect. Never let the token choose.
  2. Always pass issuer and audience to jwtVerify. Do not rely on the signature alone.
  3. Fetch verification keys from a trusted JWKS endpoint over HTTPS; createRemoteJWKSet caches and rotates keys for you.
  4. Understand that JWE gives confidentiality, not authenticity — sign when you need proof of origin.
  5. Keep the package current and monitored, since it lives at a security-critical boundary.

Get those right and jose is a dependable foundation. Almost every JWT vulnerability you read about traces back to one of the first two items on that list.

FAQ

What is the jose npm package used for?

jose is a JavaScript library for JSON Object Signing and Encryption: creating and verifying JSON Web Tokens (JWT), signatures (JWS), and encryption (JWE), and working with JSON Web Keys. It runs across Node.js, browsers, Deno, Bun, and Cloudflare Workers.

How do I verify a JWT securely with jose?

Use jwtVerify and always pass an explicit algorithms array to pin accepted algorithms, plus issuer and audience to confirm the token was issued by and for the right parties. This prevents algorithm-confusion and token-replay attacks.

What is the alg confusion attack?

It is when an attacker manipulates a JWT's declared algorithm to bypass verification, for example claiming none (no signature) or switching RS256 to HS256 so a public key is misused as an HMAC secret. Pinning accepted algorithms in jose prevents it.

Is jose better than jsonwebtoken?

jose is standards-focused, works across many runtimes, and is actively maintained, which makes it a strong modern choice. Whichever library you use, the safety rules are the same: pin algorithms and validate issuer, audience, and expiry.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.