Skip to content

[BREAKING] Modernize node-jwa: ESM + Web Crypto API#56

Open
jimmywarting wants to merge 2 commits intoauth0:masterfrom
jimmywarting:webify
Open

[BREAKING] Modernize node-jwa: ESM + Web Crypto API#56
jimmywarting wants to merge 2 commits intoauth0:masterfrom
jimmywarting:webify

Conversation

@jimmywarting
Copy link

@jimmywarting jimmywarting commented Feb 5, 2026

Summary

This pull request modernizes the node-jwa library to align with current JavaScript standards and best practices:

  • ES Modules (ESM) only - Remove CommonJS support for cleaner, standard-compliant code
  • Web Crypto API - Replace Node.js-specific crypto module with the standards-based Web Crypto API
  • Async/await - All cryptographic operations are now async (non-blocking)
  • Zero dependencies - Remove ecdsa-sig-formatter dependency with a custom, lightweight DER parser (just to get rid of safe-buffer)
  • Named exports - Use named exports for better tree-shaking and clarity

Motivation

The library was built on Node.js 0.11 APIs that are now outdated. This modernization:

  1. Improves compatibility - Web Crypto API is available in modern Node.js (>=18), browsers, Deno, and other runtimes
  2. Reduces dependencies - Removes the ecdsa-sig-formatter package entirely (~200 lines + safe-buffer size → ~50 lines custom code)
  3. Better performance - Native crypto operations are optimized by the runtime
  4. Future-proof - Code can run in browsers and other JavaScript environments
  5. Type support - Built with JSDoc instead of having any complicated build system

Major Changes

Breaking Changes (v2.x → v3.x)

1. ESM Only

// Before (v2.x)
const jwa = require('jwa')

// After (v3.x)
import { jwa } from 'jwa'

2. Async Operations

All sign() and verify() methods now return Promises:

// Before (v2.x)
const signature = algo.sign(message, key)
const isValid = algo.verify(message, signature, key)

// After (v3.x)
const signature = await algo.sign(message, key)
const isValid = await algo.verify(message, signature, key)

3. Web Crypto API

Uses globalThis.crypto.subtle instead of require('crypto'):

  • No Node-specific globals like Buffer in the implementation
  • Works in browsers, Node.js 18+, and other runtimes with Web Crypto support

Minor Changes

  • Removed ecdsa-sig-formatter - Custom ~50-line DER parser replaces it - only b/c it used safe-buffer
  • Native Uint8Array base64 methods - Uses Uint8Array.toBase64() / fromBase64() with fallbacks for older environments
  • TypedArray instead of Buffer - Uses Uint8Array throughout, better cross compatibility

Benefits

For Users

  • ✅ Can use in browsers and Node.js with same code
  • ✅ Async operations don't block the event loop
  • ✅ Smaller bundle size (zero production dependencies)
  • ✅ Modern, readable code
  • ✅ Typing support

For Maintainers

  • ✅ Fewer dependencies to maintain - all dev and devDep removed (except one)
  • ✅ Uses NodeJS new test runner and NodeJS built in node:assert instead of tap
  • ✅ Standard APIs are well-documented
  • ✅ Easier to understand and debug
  • ✅ Better test coverage with RFC 7515 vectors

Testing

All 5 RFC 7515 test vectors pass:

  • ✔ A.1 - HMAC (HS256)
  • ✔ A.2 - RSA (RS256)
  • ✔ A.3 - ECDSA (ES256)
  • ✔ A.4 - ECDSA with SHA-512 (ES512)
  • ✔ A.5 - Unsigned (none)
npm test
✔ A.1, A.2, A.3, A.4, A.5 (5 tests pass)

Migration Guide for Users

Step 1: Update package.json

npm install jwa@3.0.0

Step 2: Update imports

// Old
const jwa = require('jwa')

// New
import { jwa } from 'jwa'

Step 3: Update API calls

// Old
const sig = algo.sign(message, secret)

// New
const sig = await algo.sign(message, secret)

Step 4: Use in async context

async function signJWT(payload, secret) {
  const algo = jwa('HS256')
  return await algo.sign(JSON.stringify(payload), secret)
}

Requirements

  • Node.js 18.0.0 or later (or any runtime with Web Crypto API)
  • ES Modules support (no CommonJS)

Algorithm Support (Unchanged)

All existing algorithms continue to work:

Algorithm Type Status
HS256, HS384, HS512 HMAC
RS256, RS384, RS512 RSA
PS256, PS384, PS512 RSA-PSS
ES256, ES384, ES512 ECDSA
none No signature

Implementation Details

DER Parser for ECDSA

Replaced the entire ecdsa-sig-formatter dependency with a custom ~50-line parser that:

  • Converts DER-encoded ECDSA signatures to JOSE format (r || s)
  • Handles P-256, P-384, and P-521 curves
  • Minimal, focused implementation

Base64URL Encoding

Uses native methods with fallbacks:

// Modern runtimes
Uint8Array.toBase64({ alphabet: 'base64url' })
Uint8Array.fromBase64(str, { alphabet: 'base64url' })

// Older environments (fallback)
// Uses btoa/atob with manual replacements

Files Changed

  • index.js - Complete rewrite with Web Crypto API
  • package.json - Updated to v3.0.0, removed dependencies
  • README.md - Updated examples and documentation
  • test/*.js - Updated to use async/await and named imports
  • Removed: sync.js (experimental worker-based wrapper)

No Production Dependencies

{
  "dependencies": {},
  "devDependencies": {
    "jwk-to-pem": "^2.0.5"
  }
}

Backward Compatibility

This is a breaking change. Projects using node-jwa will need to:

  1. Upgrade to Node.js 18+ (newer version have gotten support for require(esm)
  2. Switch to ES Modules
  3. Use async/await for sign/verify operations
  4. Use named import syntax

A v3.x branch can be maintained if long-term compatibility is needed.

Questions?

This modernization aligns node-jwa with:

  • Web Crypto API standard (MDN, W3C)
  • Current Node.js best practices
  • Modern JavaScript conventions (ESM, async/await)

The implementation maintains 100% compatibility with RFC 7515 test vectors while providing a cleaner, standards-based API.

Rewrite library to ES Modules and Web Crypto API: convert index.js to ESM, replace Node crypto APIs with SubtleCrypto, add a small DER->JOSE ECDSA parser, and make all sign/verify functions async (return Promises). Update package.json for v4.0.0 (type: module, exports, node>=18), remove legacy dependencies and CI (.travis.yml), and update Makefile to use `node --test`. Tests and examples migrated to node:test and ESM, README updated for breaking changes, and PR.md added documenting the modernization and migration notes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant