These packages handle JSON Web Tokens (JWT) and related cryptographic standards (JOSE) in JavaScript. jose is the modern, zero-dependency standard for new projects. jsonwebtoken is the legacy workhorse widely found in existing codebases. jws and jwa are low-level building blocks for signatures and algorithms, typically used internally by higher-level libraries. node-jose is an older implementation that has largely been superseded by jose. Choosing the right one depends on your security requirements, need for modern standards like ESM, and whether you are maintaining legacy systems or starting fresh.
When implementing authentication or secure data exchange in Node.js, you will likely encounter JSON Web Tokens (JWT) and the broader JOSE standards. The ecosystem offers several packages, but they serve very different purposes. jose and jsonwebtoken are the main contenders for full token handling, while jws, jwa, and node-jose occupy specific niches or legacy spaces. Let's break down how they differ in security, API design, and real-world usage.
Security is the most critical factor when choosing a crypto library. How a package handles algorithms can prevent severe vulnerabilities like algorithm confusion attacks.
jose enforces strict algorithm selection. You must explicitly define the algorithm in the protected header. It does not allow the token itself to dictate the verification algorithm, which prevents attackers from switching algorithms to bypass security.
// jose: Explicit algorithm definition
import { jwtVerify } from 'jose';
const { payload } = await jwtVerify(token, key, {
algorithms: ['RS256'] // Must specify allowed algorithms
});
jsonwebtoken allows you to specify algorithms, but older versions were vulnerable if options were not passed correctly. It requires careful configuration to ensure the algorithms option is always provided during verification.
// jsonwebtoken: Options required for security
const jwt = require('jsonwebtoken');
const payload = jwt.verify(token, key, {
algorithms: ['RS256'] // Critical to prevent algorithm confusion
});
jws provides low-level signature verification but does not validate JWT claims like exp or iss. You must implement these checks manually, which increases the risk of human error.
// jws: Manual validation required
const jws = require('jws');
const valid = jws.verify(token, 'HS256', secret);
// You must manually decode and check expiration claims
jwa focuses purely on algorithm support. It does not verify tokens. It is used to check if an algorithm is supported or to perform raw signing operations.
// jwa: Algorithm utility
const jwa = require('jwa');
const hmac = jwa('HS256');
// Only provides sign/verify methods for raw data
node-jose supports JOSE standards but lacks the modern security defaults found in jose. It requires more boilerplate to ensure secure configurations.
// node-jose: Verbose security setup
const jose = require('node-jose');
// Requires manual key management and algorithm checks
Cryptographic operations should ideally be asynchronous to avoid blocking the Node.js event loop. This is a key differentiator between modern and legacy libraries.
jose is fully asynchronous. All signing and verification operations return Promises. This ensures that heavy crypto math does not stall your server's ability to handle other requests.
// jose: Async API
import { SignJWT } from 'jose';
const jwt = await new SignJWT({ userId: 123 })
.setProtectedHeader({ alg: 'HS256' })
.sign(secret);
jsonwebtoken uses a synchronous API by default. While this is simpler to write, it can block the event loop during high load. It does offer an async callback version, but the default usage is sync.
// jsonwebtoken: Sync by default
const jwt = require('jsonwebtoken');
const token = jwt.sign({ userId: 123 }, secret); // Blocks event loop
jws is also synchronous for its main methods. This shares the same performance risks as jsonwebtoken when used for heavy operations.
// jws: Sync signing
const jws = require('jws');
const token = jws.sign({
header: { alg: 'HS256' },
payload: { userId: 123 },
secret: secret
});
jwa provides synchronous sign and verify methods for raw data.
// jwa: Sync operations
const sig = hmac.sign('data', secret);
node-jose uses Promises, similar to jose, but the API is more verbose and less intuitive.
// node-jose: Async but verbose
const sig = await jose.JWS.createSign({ format: 'compact' }, key)
.update({ userId: 123 })
.final();
Handling keys securely is complex. Libraries differ in how they accept secrets, PEM files, and JWKs (JSON Web Keys).
jose has excellent support for various key formats including PEM, JWK, and CryptoKey. It integrates well with the Web Crypto API.
// jose: Flexible key import
import { importSPKI } from 'jose';
const publicKey = await importSPKI(pemPublicKey, 'RS256');
jsonwebtoken accepts strings or buffers for secrets and keys. It is less strict about key formats, which can sometimes lead to configuration errors.
// jsonwebtoken: String or Buffer
const payload = jwt.verify(token, publicKeyString);
jws expects secrets or keys as strings or buffers. It does not handle JWKs natively without extra work.
// jws: Raw secret
jws.verify(token, algorithm, secretBuffer);
jwa does not manage keys; it operates on raw secrets passed to the sign function.
// jwa: Raw secret passed directly
hmac.sign(data, secret);
node-jose has robust JWK support, which was a key feature in its prime, but jose now matches this with a cleaner API.
// node-jose: JWK support
const key = await jose.JWK.asKey(jwkObject);
For backend services, dependencies matter less than for frontend bundles, but fewer dependencies mean a smaller attack surface.
jose has zero dependencies. This reduces the risk of supply chain attacks and simplifies installation.
jsonwebtoken depends on jws, jwa, and lodash. More dependencies mean more potential maintenance issues.
jws depends on jwa and safe-buffer. It is lightweight but still pulls in external code.
jwa has no dependencies. It is a tiny utility library.
node-jose has multiple dependencies including lodash and buffer. It is heavier than jose.
You are building a new auth service from scratch.
jose// jose: New service implementation
import { SignJWT, jwtVerify } from 'jose';
export async function createToken(user) {
return await new SignJWT(user)
.setProtectedHeader({ alg: 'HS256' })
.sign(process.env.JWT_SECRET);
}
You are updating an older Express app that already uses JWT.
jsonwebtoken// jsonwebtoken: Legacy maintenance
app.post('/login', (req, res) => {
const token = jwt.sign(req.user, process.env.SECRET);
res.json({ token });
});
You are implementing a non-standard signed message format.
jws or jwa// jws: Custom signature
const signature = jws.sign({
header: { alg: 'HS256' },
payload: customData,
secret: key
});
You are integrating with an older OpenID Connect provider.
node-josejose is preferred if you can control the integration.// node-jose: Legacy OIDC
const keystore = await jose.JWK.createKeyStore();
| Package | Maintenance | API Style | Dependencies | Best Use Case |
|---|---|---|---|---|
jose | ✅ Active | Async | 0 | New projects, high security |
jsonwebtoken | ✅ Active | Sync | Multiple | Legacy maintenance |
jws | ✅ Active | Sync | Few | Custom signatures |
jwa | ✅ Active | Sync | 0 | Algorithm utilities |
node-jose | ⚠️ Legacy | Async | Multiple | Older OIDC systems |
For almost all modern development, jose is the clear choice. It aligns with current security best practices, avoids blocking the event loop, and has no dependencies. It is the successor to the functionality found in node-jose and jsonwebtoken.
Reserve jsonwebtoken for maintaining existing systems where refactoring auth logic is too costly. Avoid using jws and jwa directly for authentication unless you are an cryptography expert building custom protocols, as manual implementation often leads to security gaps.
Migrate away from node-jose when possible. While it still works, the community has consolidated around jose for future updates and support.
Choose jose for all new projects requiring JWT or JOSE support. It is actively maintained, supports both ESM and CommonJS, and enforces async crypto operations for better security. It is the current industry standard recommended by security experts for its robust API and lack of dependencies.
Choose jsonwebtoken only if you are maintaining an existing codebase that already relies on it. While still maintained, it uses synchronous crypto by default which can block the event loop, and it lacks native ESM support compared to jose. Avoid using it for new greenfield projects.
Choose jwa only if you are building a custom cryptographic protocol and need low-level algorithm validation or signing primitives. It is not suitable for standard JWT authentication flows because it does not handle token structure, expiration, or claims validation out of the box.
Choose jws if you need to implement custom JSON Web Signature logic without the overhead of full JWT claims handling. Like jwa, it is a low-level utility. For standard authentication, prefer jose or jsonwebtoken to avoid security pitfalls associated with manual signature implementation.
Do not choose node-jose for new projects. It is considered legacy software and has been superseded by the jose package. Existing projects using it should plan a migration to jose to ensure continued security updates and compatibility with modern Node.js features.
jose is a JavaScript module for JSON Object Signing and Encryption, providing support for JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), JSON Web Key Set (JWKS), and more. The module is designed to work across various Web-interoperable runtimes including Node.js, browsers, Cloudflare Workers, Deno, Bun, and others.
If you want to quickly add JWT authentication to JavaScript apps, feel free to check out Auth0's JavaScript SDK and free plan. Create an Auth0 account; it's free!
Support from the community to continue maintaining and improving this module is welcome. If you find the module useful, please consider supporting the project by becoming a sponsor.
jose has no dependencies and it exports tree-shakeable ESM1.
jose is distributed via npmjs.com, jsr.io, jsdelivr.com, and github.com.
example ESM import1
import * as jose from 'jose'
The jose module supports JSON Web Tokens (JWT) and provides functionality for signing and verifying tokens, as well as their JWT Claims Set validation.
jwtVerify function
SignJWT classThe jose module supports encrypted JSON Web Tokens and provides functionality for encrypting and decrypting tokens, as well as their JWT Claims Set validation.
jwtDecrypt functionEncryptJWT classThe jose module supports importing, exporting, and generating keys and secrets in various formats, including PEM formats like SPKI, X.509 certificate, and PKCS #8, as well as JSON Web Key (JWK).
The jose module supports signing and verification of JWS messages with arbitrary payloads in Compact, Flattened JSON, and General JSON serialization syntaxes.
The jose module supports encryption and decryption of JWE messages with arbitrary plaintext in Compact, Flattened JSON, and General JSON serialization syntaxes.
The following are additional features and utilities provided by the jose module:
The jose module is compatible with JavaScript runtimes that support the utilized Web API globals and standard built-in objects or are Node.js.
The following runtimes are supported (this is not an exhaustive list):
Please note that certain algorithms may not be available depending on the runtime used. You can find a list of available algorithms for each runtime in the specific issue links provided above.
| Version | Security Fixes 🔑 | Other Bug Fixes 🐞 | New Features ⭐ | Runtime and Module type |
|---|---|---|---|---|
| v6.x | Security Policy | ✅ | ✅ | Universal2 ESM1 |
The algorithm implementations in jose have been tested using test vectors from their respective specifications as well as RFC7520.