UNPKG

simple-jwt-id

Version:

A lightweight JWT library with multiple algorithm support and token revocation

733 lines (549 loc) โ€ข 18.9 kB
# simple-jwt-id [![npm version](https://img.shields.io/npm/v/simple-jwt-id.svg?style=flat-square)](https://www.npmjs.com/package/simple-jwt-id) [![npm downloads](https://img.shields.io/npm/dm/simple-jwt-id.svg?style=flat-square)](https://www.npmjs.com/package/simple-jwt-id) [![license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) [![CI](https://github.com/ibnushahraa/simple-jwt-id/actions/workflows/test.yml/badge.svg)](https://github.com/ibnushahraa/simple-jwt-id/actions) [![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen.svg?style=flat-square)](https://github.com/ibnushahraa/simple-jwt-id) ๐Ÿ” A lightweight **JWT (JSON Web Token)** library for Node.js with **multiple algorithm support**, **token revocation**, and **zero configuration**. Think of it as a **simple yet powerful JWT handler** with built-in blacklist support. --- ## โœจ Features - Create and verify JWT tokens with ease - **Refresh token support** with automatic rotation - Support for multiple algorithms: **HS256, HS512, RS256, RS512, PS256, ES256** - Built-in **token revocation** with automatic blacklist management - Standard JWT claims: **iat, exp, nbf, aud, iss, sub, jti** - Decode tokens without verification - Custom error classes for better error handling - TypeScript definitions included - Works with both **CommonJS** and **ES Modules** - Minimal dependencies (only [simple-cache-id](https://www.npmjs.com/package/simple-cache-id) for blacklist) --- ## ๐Ÿ“ฆ Installation ```bash npm install simple-jwt-id ``` --- ## ๐Ÿš€ Usage ### Basic Usage (CommonJS) ```js const { createToken, verifyToken, decodeToken } = require("simple-jwt-id"); const SECRET = "your-secret-key"; (async () => { // Create a token const token = await createToken( { userId: 123, username: "john_doe" }, SECRET, { expiresIn: 3600 } // 1 hour ); console.log("Token:", token); // Verify token const payload = await verifyToken(token, SECRET); console.log("Payload:", payload); // Decode without verification const decoded = decodeToken(token); console.log("Decoded:", decoded); })(); ``` ### ES Modules ```js import { createToken, verifyToken, decodeToken } from "simple-jwt-id"; const SECRET = "your-secret-key"; // Create token with custom claims const token = await createToken({ userId: 789, role: "admin" }, SECRET, { algorithm: "HS512", expiresIn: 7200, // 2 hours issuer: "my-app", audience: "my-api", }); // Verify with validation const payload = await verifyToken(token, SECRET, { audience: "my-api", issuer: "my-app", }); ``` ### Token Revocation ```js const { createToken, verifyToken, revokeToken } = require("simple-jwt-id"); const SECRET = "your-secret-key"; (async () => { // Create token const token = await createToken({ userId: 456 }, SECRET); // Verify - works fine await verifyToken(token, SECRET); // Revoke token await revokeToken(token); // Verify again - throws error try { await verifyToken(token, SECRET); } catch (error) { console.log(error.message); // "Token revoked" } })(); ``` ### Refresh Token (Access + Refresh Token Pair) ```js const { createTokenPair, refreshTokens, verifyToken, } = require("simple-jwt-id"); const SECRET = "your-secret-key"; (async () => { // Step 1: Login - Create token pair const { accessToken, refreshToken } = await createTokenPair( { userId: 123, role: "admin" }, SECRET, { accessExpiresIn: 900, // 15 minutes refreshExpiresIn: 604800, // 7 days } ); console.log("Access Token:", accessToken); console.log("Refresh Token:", refreshToken); // Step 2: Use access token for API calls const payload = await verifyToken(accessToken, SECRET, { audience: "api", }); console.log("Verified:", payload); // Step 3: When access token expires, refresh it const { accessToken: newAccessToken, refreshToken: newRefreshToken, } = await refreshTokens(refreshToken, SECRET); console.log("New Access Token:", newAccessToken); console.log("New Refresh Token:", newRefreshToken); // Step 4: Old refresh token is now revoked (rotation enabled by default) try { await verifyToken(refreshToken, SECRET, { audience: "refresh" }); } catch (error) { console.log(error.message); // "Token revoked" } })(); ``` ### Refresh Token Without Rotation ```js const { createTokenPair, refreshTokens } = require("simple-jwt-id"); const SECRET = "your-secret-key"; (async () => { // Create token pair const { refreshToken } = await createTokenPair( { userId: 456 }, SECRET ); // Refresh without rotation - old refresh token remains valid const { accessToken } = await refreshTokens(refreshToken, SECRET, { rotateRefreshToken: false, }); console.log("New Access Token:", accessToken); // Old refresh token can still be used const result = await refreshTokens(refreshToken, SECRET, { rotateRefreshToken: false, }); console.log("Another Access Token:", result.accessToken); })(); ``` ### Advanced Usage ```js const { createToken, verifyToken, decodeToken, TokenExpiredError, NotBeforeError, } = require("simple-jwt-id"); const SECRET = "your-secret-key"; (async () => { // Token with notBefore claim const token = await createToken({ action: "verify-email" }, SECRET, { expiresIn: 600, // 10 minutes notBefore: 60, // Valid after 1 minute subject: "user@example.com", }); // Try to use immediately - throws NotBeforeError try { await verifyToken(token, SECRET); } catch (error) { if (error instanceof NotBeforeError) { console.log("Token not yet valid!"); } } // Decode with complete info const { header, payload } = decodeToken(token, { complete: true }); console.log("Algorithm:", header.alg); console.log("Subject:", payload.sub); // Ignore expiration (useful for debugging) const expiredToken = await createToken({ test: true }, SECRET, { expiresIn: 1, }); await new Promise((resolve) => setTimeout(resolve, 2000)); // This would throw TokenExpiredError // await verifyToken(expiredToken, SECRET); // But this works const payload = await verifyToken(expiredToken, SECRET, { ignoreExpiration: true, }); console.log("Payload:", payload); })(); ``` --- ## ๐Ÿงช Testing ```bash npm test ``` Jest is used for testing. All tests must pass before publishing. **Test Coverage:** - 68 unit tests covering all features - Both CommonJS and ES Module tests - Token creation, verification, decoding, and revocation - Refresh token functionality with rotation - Error handling and edge cases - Complete auth flow integration tests --- ## ๐Ÿ“‚ Project Structure ``` src/ โ†’ main source code โ”œโ”€โ”€ index.js โ†’ CommonJS version โ””โ”€โ”€ index.mjs โ†’ ES Module version test/ โ†’ jest test suite โ”œโ”€โ”€ test-require.test.js โ†’ CommonJS tests โ””โ”€โ”€ test-import.test.mjs โ†’ ES Module tests examples/ โ†’ usage examples โ”œโ”€โ”€ example-require.js โ†’ CommonJS example โ””โ”€โ”€ example-import.mjs โ†’ ES Module example index.d.ts โ†’ TypeScript definitions ``` --- ## ๐Ÿ“œ API ### `createToken(payload, secret, options?)` Create a new JWT token. **Parameters:** - `payload` (object): The payload data to include in the token - `secret` (string | Buffer): Secret key or private key for signing - `options` (object, optional): - `algorithm` (string): Algorithm to use - `HS256`, `HS512`, `RS256`, `RS512`, `PS256`, `ES256` (default: `HS256`) - `expiresIn` (number): Token expiration time in seconds (default: `3600`) - `notBefore` (number): Token not valid before this time offset in seconds - `audience` (string): Intended audience for the token - `issuer` (string): Token issuer - `subject` (string): Token subject **Returns:** Promise resolving to the signed JWT token string **Example:** ```js const token = await createToken({ userId: 123, role: "admin" }, "secret-key", { algorithm: "HS256", expiresIn: 7200, // 2 hours issuer: "my-app", audience: "my-api", subject: "user-authentication", }); ``` ### `verifyToken(token, secret, options?)` Verify and validate a JWT token. **Parameters:** - `token` (string): The JWT token to verify - `secret` (string | Buffer): Secret key or public key for verification - `options` (object, optional): - `ignoreExpiration` (boolean): If true, ignores token expiration (default: `false`) - `audience` (string): Expected audience to validate against - `issuer` (string): Expected issuer to validate against **Returns:** Promise resolving to the verified token payload **Throws:** - `JsonWebTokenError`: Invalid token format, signature, audience, issuer, or revoked token - `TokenExpiredError`: Token has expired - `NotBeforeError`: Token is not yet valid (nbf claim) **Example:** ```js try { const payload = await verifyToken(token, "secret-key", { audience: "my-api", issuer: "my-app", }); console.log("User ID:", payload.userId); } catch (error) { if (error instanceof TokenExpiredError) { console.log("Token expired!"); } else if (error instanceof NotBeforeError) { console.log("Token not yet valid!"); } else { console.log("Invalid token:", error.message); } } ``` ### `decodeToken(token, options?)` Decode a JWT token without verifying its signature. **Parameters:** - `token` (string): The JWT token to decode - `options` (object, optional): - `complete` (boolean): If true, returns both header and payload; if false, returns only payload (default: `false`) **Returns:** The decoded token payload, or `{ header, payload }` if complete is true **Throws:** - `JsonWebTokenError`: Invalid token format **Example:** ```js // Decode payload only const payload = decodeToken(token); console.log(payload); // { userId: 123, iat: 1234567890, exp: 1234571490, ... } // Decode with complete info const { header, payload } = decodeToken(token, { complete: true }); console.log(header); // { alg: 'HS256', typ: 'JWT' } console.log(payload); // { userId: 123, ... } ``` ### `revokeToken(token)` Revoke a JWT token by adding it to the blacklist. **Parameters:** - `token` (string): The JWT token to revoke **Returns:** Promise<void> **Description:** Adds the token's jti (JWT ID) to a blacklist cache. The token will remain blacklisted until its expiration time. Uses [simple-cache-id](https://www.npmjs.com/package/simple-cache-id) for efficient in-memory blacklist management. **Example:** ```js // Revoke a token (e.g., on user logout) await revokeToken(token); // Subsequent verification attempts will fail await verifyToken(token, SECRET); // Throws: "Token revoked" ``` ### `createTokenPair(payload, secret, options?)` Creates a pair of access and refresh tokens. **Parameters:** - `payload` (object): The payload data to include in both tokens - `secret` (string | Buffer): Secret key or private key for signing - `options` (object, optional): - `algorithm` (string): Algorithm to use (default: `HS256`) - `accessExpiresIn` (number): Access token expiration in seconds (default: `900` - 15 minutes) - `refreshExpiresIn` (number): Refresh token expiration in seconds (default: `604800` - 7 days) - `accessAudience` (string): Audience for access token (default: `api`) - `refreshAudience` (string): Audience for refresh token (default: `refresh`) - `issuer` (string): Token issuer - `subject` (string): Token subject **Returns:** Promise resolving to `{ accessToken, refreshToken }` **Example:** ```js const { accessToken, refreshToken } = await createTokenPair( { userId: 123, role: "admin" }, "secret-key", { accessExpiresIn: 900, // 15 minutes refreshExpiresIn: 604800, // 7 days issuer: "auth-service", } ); ``` ### `refreshTokens(refreshToken, secret, options?)` Refreshes tokens using a valid refresh token. **Parameters:** - `refreshToken` (string): The refresh token to use - `secret` (string | Buffer): Secret key or public key for verification - `options` (object, optional): - `algorithm` (string): Algorithm for new tokens (default: `HS256`) - `accessExpiresIn` (number): New access token expiration (default: `900`) - `refreshExpiresIn` (number): New refresh token expiration (default: `604800`) - `accessAudience` (string): Audience for access token (default: `api`) - `refreshAudience` (string): Expected audience for validation (default: `refresh`) - `issuer` (string): Expected issuer to validate - `rotateRefreshToken` (boolean): Whether to generate new refresh token (default: `true`) **Returns:** Promise resolving to `{ accessToken, refreshToken? }` **Throws:** - `JsonWebTokenError`: Invalid or revoked refresh token - `TokenExpiredError`: Refresh token has expired - `NotBeforeError`: Refresh token not yet valid **Example:** ```js // With rotation (default) - returns both tokens const { accessToken, refreshToken } = await refreshTokens( oldRefreshToken, "secret-key" ); // Without rotation - returns only access token const { accessToken } = await refreshTokens(oldRefreshToken, "secret-key", { rotateRefreshToken: false, }); ``` --- ## ๐Ÿ” Algorithms ### HMAC Algorithms (Symmetric) - **HS256** - HMAC using SHA-256 (default, recommended for most use cases) - **HS512** - HMAC using SHA-512 (more secure, slightly slower) **Usage:** ```js const token = await createToken(payload, "your-secret-key", { algorithm: "HS256", }); ``` ### RSA Algorithms (Asymmetric) - **RS256** - RSASSA-PKCS1-v1_5 using SHA-256 - **RS512** - RSASSA-PKCS1-v1_5 using SHA-512 - **PS256** - RSASSA-PSS using SHA-256 **Usage:** ```js const fs = require("fs"); const privateKey = fs.readFileSync("private.pem"); const publicKey = fs.readFileSync("public.pem"); // Sign with private key const token = await createToken(payload, privateKey, { algorithm: "RS256", }); // Verify with public key const payload = await verifyToken(token, publicKey); ``` ### ECDSA Algorithm (Asymmetric) - **ES256** - ECDSA using P-256 and SHA-256 **Usage:** ```js const token = await createToken(payload, privateKey, { algorithm: "ES256", }); ``` --- ## ๐Ÿ”‘ JWT Claims ### Standard Claims (Automatically Managed) - **`jti`** (JWT ID) - Unique token identifier (auto-generated) - **`iat`** (Issued At) - Timestamp when token was created (auto-generated) - **`exp`** (Expiration Time) - Timestamp when token expires (set via `expiresIn`) ### Optional Claims - **`nbf`** (Not Before) - Token not valid before this timestamp (set via `notBefore`) - **`aud`** (Audience) - Intended recipient of the token (set via `audience`) - **`iss`** (Issuer) - Token issuer (set via `issuer`) - **`sub`** (Subject) - Subject of the token (set via `subject`) **Example:** ```js const token = await createToken({ userId: 123 }, SECRET, { expiresIn: 3600, // exp = iat + 3600 notBefore: 60, // nbf = iat + 60 audience: "my-api", issuer: "auth-service", subject: "user-auth", }); // Payload will contain: // { // userId: 123, // jti: "a1b2c3d4e5f6g7h8", // iat: 1234567890, // exp: 1234571490, // nbf: 1234567950, // aud: "my-api", // iss: "auth-service", // sub: "user-auth" // } ``` --- ## ๐Ÿ›ก๏ธ Error Handling ### Error Classes ```js const { JsonWebTokenError, TokenExpiredError, NotBeforeError, } = require("simple-jwt-id"); ``` - **`JsonWebTokenError`** - Base error class for all JWT errors - Invalid token format - Invalid signature - Invalid audience - Invalid issuer - Token revoked - **`TokenExpiredError`** extends `JsonWebTokenError` - Token has expired (current time > exp) - **`NotBeforeError`** extends `JsonWebTokenError` - Token used before valid time (current time < nbf) ### Error Handling Example ```js try { const payload = await verifyToken(token, SECRET); } catch (error) { if (error instanceof TokenExpiredError) { // Handle expired token - maybe refresh it console.log("Token expired, please login again"); } else if (error instanceof NotBeforeError) { // Handle token not yet valid console.log("Token not yet valid, please wait"); } else if (error instanceof JsonWebTokenError) { // Handle other JWT errors console.log("Invalid token:", error.message); } else { // Handle unexpected errors console.error("Unexpected error:", error); } } ``` --- ## ๐Ÿ—„๏ธ Token Revocation Token revocation is handled automatically using [simple-cache-id](https://www.npmjs.com/package/simple-cache-id) for efficient in-memory blacklist management. **How it works:** 1. Each token has a unique `jti` (JWT ID) claim 2. When you call `revokeToken()`, the `jti` is added to an in-memory blacklist 3. The blacklist entry expires when the token would naturally expire (based on `exp` claim) 4. Verification checks the blacklist before validating the token **Benefits:** - No database required for basic revocation - Automatic cleanup when tokens expire - Memory-efficient (only stores JTI, not full token) - Optional persistent storage support (via simple-cache-id) **Example Use Cases:** - User logout - Password reset - Permission changes - Security incidents ```js // User logs out await revokeToken(userToken); // Change user permissions - revoke all existing tokens await revokeToken(token1); await revokeToken(token2); await revokeToken(token3); // Security incident - revoke compromised token await revokeToken(compromisedToken); ``` --- ## ๐Ÿ’ก TypeScript Support TypeScript definitions are included out of the box! ```typescript import { createToken, verifyToken, decodeToken, revokeToken, type JWTPayload, type CreateTokenOptions, type VerifyTokenOptions, JsonWebTokenError, TokenExpiredError, NotBeforeError, } from "simple-jwt-id"; interface UserPayload extends JWTPayload { userId: number; username: string; role: string; } const payload: UserPayload = { userId: 123, username: "john_doe", role: "admin", }; const options: CreateTokenOptions = { algorithm: "HS256", expiresIn: 3600, issuer: "my-app", }; const token = await createToken(payload, "secret", options); try { const verified = await verifyToken(token, "secret"); console.log(verified.userId); // TypeScript knows this exists } catch (error) { if (error instanceof TokenExpiredError) { console.log("Token expired!"); } } ``` --- ## ๐Ÿ“„ License [MIT](LICENSE) ยฉ 2025 --- ## ๐Ÿ™ Acknowledgments - Uses [simple-cache-id](https://www.npmjs.com/package/simple-cache-id) for efficient token blacklist management - Inspired by [jsonwebtoken](https://www.npmjs.com/package/jsonwebtoken) with a focus on simplicity and modern JavaScript --- ## ๐Ÿ“š Resources - [JWT.io](https://jwt.io/) - Learn more about JSON Web Tokens - [RFC 7519](https://tools.ietf.org/html/rfc7519) - JWT Specification - [RFC 7515](https://tools.ietf.org/html/rfc7515) - JWS Specification