Toolbox-XToolbox-X

Sign, Decode, Verify Tokens

A lightweight, secure implementation of JWT-like tokens using HMAC-SHA256 signatures.

Signet

A lightweight, secure implementation of JWT-like tokens using HMAC-SHA256 signatures.

Info

Does not rely on Node.js or Web APIs. Works on any JS engine

Overview

Signet is a zero-dependency token implementation similar to JSON Web Tokens (JWT) but with a smaller footprint and simpler API. It provides secure token creation, verification, and decoding with HMAC-SHA256 signatures and standard JWT claims.

Import & Setup

import { Signet } from 'toolbox-x/hash';

Basic Usage

playground.ts

Token Structure

Signet tokens follow the standard JWT format: header.payload.signature

{
  "alg": "HS256",
  "typ": "SIGNET+JWT"
}

Payload

Includes standard JWT claims plus your custom data:

  • iat, iatDate: Issued at timestamp and Date object
  • exp, expDate: Expiration time (if expiresIn provided)
  • nbf, nbfDate: Not before time (if notBefore provided)
  • aud: Audience
  • sub: Subject
  • iss: Issuer
  • Custom properties from your payload

API Reference

Constructor

new Signet(secret: string)

Creates a new Signet instance with the specified secret key.

Parameters

  • secret: Non-empty string used for signing/verifying tokens

Example

const signet = new Signet(process.env.JWT_SECRET);

sign()

Creates and signs a new token.

Signature

sign(payload: GenericObject, options?: SignOptions): TokenString

Parameters

  • payload: Custom data to include in token (must be non-empty object)
  • options: Optional configuration for claims and expiration

Example

const token = signet.sign(
  { userId: 456 },
  { expiresIn: '1d', audience: 'api' }
);

verify()

Verifies token signature and validates claims.

Signature

verify<T>(token: string, options?: VerifyOptions): VerifiedToken<T>

Parameters

  • token: Token string to verify
  • options: Optional validation criteria

Returns: Object with isValid boolean and either payload or error

Example

const result = signet.verify<UserData>(token, {
  audience: 'api',
  issuer: 'auth'
});

verifyOrThrow()

Verifies token and throws error if invalid.

Signature

verifyOrThrow<T>(token: string, options?: VerifyOptions): VerifiedToken<T>

Example

try {
  const result = signet.verifyOrThrow(token);
  // Token is valid
} catch (error) {
  // Handle invalid token
}

decode()

Decodes token without verifying signature.

Signature

decode<T>(token: string): DecodedToken<T>

Example

const decoded = signet.decode<{ userId: number }>(token);
console.log(decoded.payload.userId); // Type-safe access

decodePayload()

Extracts payload without verification (convenience method).

Signature

decodePayload<T>(token: string): SignetPayload<T>

Example

const payload = signet.decodePayload(token);
console.log(payload.iatDate);

Validation Methods

hasExpired()

hasExpired(token: string): boolean

Checks if token has expired based on exp claim.

isTooEarly()

isTooEarly(token: string): boolean

Checks if token's nbf claim indicates it's not yet valid.

isInvalidIssuer()

isInvalidIssuer(token: string, expected: string): boolean

Validates issuer claim.

isInvalidAudience()

isInvalidAudience(token: string, expected: string | string[]): boolean

Validates audience claim.

isInvalidSubject()

isInvalidSubject(token: string, expected: string): boolean

Validates subject claim.

Time Formats

Signet accepts flexible time formats for expiresIn and notBefore:

// Various time formats
signet.sign(payload, { expiresIn: '2h' });        // 2 hours
signet.sign(payload, { expiresIn: '30m' });       // 30 minutes  
signet.sign(payload, { expiresIn: '1d' });        // 1 day
signet.sign(payload, { expiresIn: '3600' });      // 3600 seconds
signet.sign(payload, { expiresIn: 7200 });        // 7200 seconds

Supported Units

UnitVariants
Yeary, yr, yrs, year, years
Monthmo, month, months
Weekw, week, weeks
Dayd, day, days
Hourh, hr, hrs, hour, hours
Minutem, min, mins, minute, minutes
Seconds, sec, secs, second, seconds
Millisecondms, msec, msecs, millisecond, milliseconds

Info

Internally uses parseMSec utility to parse time to ms

TypeScript Support

Signet provides full TypeScript support with generic types:

interface UserToken {
  userId: number;
  email: string;
  permissions: string[];
}

// Type-safe token creation: payload must be an object type
const token = signet.sign({
  userId: 123,
  email: 'user@example.com',
  permissions: ['read', 'write']
});

// Type-safe verification  
const result = signet.verify<UserToken>(token);
if (result.isValid) {
  const { userId, permissions } = result.payload; // Correctly typed
}

Security Features

HMAC-SHA256 Signatures

  • Uses cryptographically secure HMAC-SHA256 for signatures
  • Constant-time signature comparison prevents timing attacks

Claim Validation

  • Automatic expiration checking
  • Not-before validation
  • Issuer, audience, and subject validation
  • All validations are optional and configurable

Secure by Default

  • Requires non-empty secret
  • Validates token structure
  • Handles malformed tokens gracefully

Common Patterns

Express.js Middleware

function authMiddleware(signet: Signet) {
  return (req: Request, res: Response, next: NextFunction) => {
    const authHeader = req.headers.authorization;
    const token = authHeader?.replace('Bearer ', '');

    if (!token) {
      return res.status(401).json({ error: 'No token provided' });
    }

    try {
      const result = signet.verifyOrThrow(token, {
        audience: 'api',
        issuer: 'auth-service'
      });
      req.user = result.payload;
      next();
    } catch (error) {
      res.status(401).json({ error: error.message });
    }
  };
}

Refresh Token Pattern

function refreshAccessToken(refreshToken: string): string | null {
  const result = signet.verify(refreshToken, {
    audience: 'refresh',
    issuer: 'auth-service'
  });

  if (!result.isValid) {
    return null;
  }

  // Create new access token
  return signet.sign(
    { userId: result.payload.userId },
    { expiresIn: '15m', audience: 'access' }
  );
}

Multi-Tenant Applications

function validateTenantToken(token: string, tenantId: string): boolean {
  const result = signet.verify(token);
  
  if (!result.isValid) {
    return false;
  }

  // Check if token has required tenant scope
  const scopes = result.payload.scopes || [];
  return scopes.includes(`tenant:${tenantId}`);
}

Error Handling

Verification Errors

The verify method returns an error object:

const result = signet.verify(token);

if (!result.isValid) {
  switch (result.error) {
    case 'Invalid or tampered signature!':
      // Token was modified
      break;
    case 'Token has expired!':
      // Token expired, need refresh
      break;
    case 'Invalid token audience!':
      // Wrong audience
      break;
    default:
      // Other errors
  }
}

Exception Handling with verifyOrThrow

try {
  const result = signet.verifyOrThrow(token);
  // Process valid token
} catch (error) {
  if (error.message.includes('expired')) {
    // Handle expired token
  } else if (error.message.includes('audience')) {
    // Handle wrong audience
  } else {
    // Handle other errors
  }
}

Performance & Considerations

Lightweight

  • Zero dependencies
  • Pure JavaScript implementation
  • Small bundle size

Memory Efficient

  • Uses Uint8Array for binary operations
  • Minimal object allocations
  • Efficient string operations

Suitable For

  • API authentication
  • Session management
  • One-time use tokens
  • Secure data exchange

Not Suitable For

  • Very high-throughput applications (consider native crypto)
  • Tokens larger than 64KB (JWT size limit)
  • Asymmetric cryptography needs (use JWK/JWS instead)

Migration from JWT

If you're using jsonwebtoken library:

// Before (jsonwebtoken)
import jwt from 'jsonwebtoken';
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
const decoded = jwt.verify(token, secret);

// After (Signet)
import { Signet } from 'toolbox-x/hash';
const signet = new Signet(secret);
const token = signet.sign(payload, { expiresIn: '1h' });
const result = signet.verify(token);

Best Practices

1. Secure Secret Management

// Store secret in environment variables
const signet = new Signet(process.env.JWT_SECRET);

// Rotate secrets periodically
const currentSignet = new Signet(getCurrentSecret());
const legacySignet = new Signet(getLegacySecret()); // For token migration

2. Appropriate Token Lifetimes

// Access tokens: short-lived
signet.sign(payload, { expiresIn: '15m' });

// Refresh tokens: longer-lived  
signet.sign(payload, { expiresIn: '7d' });

// One-time tokens: very short
signet.sign(payload, { expiresIn: '5m' });

3. Validate Relevant Claims

// Always validate audience and issuer
signet.verify(token, {
  audience: 'your-app',
  issuer: 'your-service'
});

// Validate subject when user-specific
signet.verify(token, {
  subject: expectedUserId
});

4. Handle Token Storage Securely

// Web: Use HTTP-only cookies
res.cookie('token', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict'
});

Troubleshooting

Common Issues

Invalid or tampered signature!

  • Secret mismatch between signing and verifying
  • Token was modified after creation
  • Encoding/decoding issues

Token has expired!

  • Token past its exp claim
  • System clock skew between services

Invalid token audience!

  • Token created for different audience
  • Multi-tenant configuration mismatch

Debugging Tips

// Decode to inspect without verification
const decoded = signet.decode(token);
console.log('Header:', decoded.header);
console.log('Payload:', decoded.payload);
console.log('Expires at:', decoded.payload.expDate);

// Check specific claims
console.log('Has expired?', signet.hasExpired(token));
console.log('Valid issuer?', !signet.isInvalidIssuer(token, 'expected-issuer'));

Last updated: Sun, Jun 14, 2026 08:22:19PM (UTC)

On this page