admGitea
  • Joined on 2026-01-03

hessutech-auth-consumer (1.0.0)

Published 2026-01-04 12:50:21 +00:00 by admGitea

Installation

registry=
npm install hessutech-auth-consumer@1.0.0
"hessutech-auth-consumer": "1.0.0"

About this package

Hessu Auth Consumer

Production-ready JWT authentication middleware for Hessutech services

A reusable, platform-agnostic library for validating Hessu-JWT tokens signed by The Sentinel using RS256 asymmetric cryptography. Designed for all Hessutech services (Service B, C, etc.) that need to authenticate users without direct database access.


Features

  • RS256 JWT Verification: Asymmetric public key cryptography (no shared secrets)
  • Fastify Plugin: Drop-in authentication with automatic request decoration
  • Standalone Verifier: Use with any Node.js framework (Express, Koa, etc.)
  • Static or Dynamic Keys: Configure with environment variable or fetch from Sentinel
  • App Claim Validation: Prevent cross-service token reuse
  • TypeScript-First: Strict type safety with no any types
  • Zero Placeholders: Complete, production-ready code
  • Atomic Design: Small, focused modules following SOLID principles

Installation

npm install hessu-auth-consumer

Peer Dependencies:

  • fastify ^5.0.0 (only if using the Fastify plugin)
  • jose ^5.9.6 (JWT verification library)

Quick Start

import Fastify from 'fastify';
import { hessuAuthPlugin } from 'hessu-auth-consumer';

const server = Fastify();

// Public routes
server.get('/health', async () => ({ status: 'ok' }));

// Protected routes
await server.register(async (protectedRoutes) => {
  await protectedRoutes.register(hessuAuthPlugin, {
    publicKey: process.env.HESSU_PUBLIC_KEY,
    expectedApp: 'service-b'
  });

  protectedRoutes.get('/api/me', async (request) => {
    // request.user is automatically populated
    return {
      userId: request.user.sub,
      roles: request.user.roles
    };
  });
});

await server.listen({ port: 3001 });

Option 2: Standalone Verifier

import { createVerifier } from 'hessu-auth-consumer';

const verifier = createVerifier({
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b'
});

// In your middleware or route handler
const authHeader = request.headers.authorization;
const token = verifier.extractToken(authHeader);
const user = await verifier.verify(token);

console.log(user.sub, user.roles);

Configuration

Environment Variables

Create a .env file in your consuming service:

# Required: Your service's unique identifier
SERVICE_APP_ID=service-b

# Option A: Static public key (recommended for production)
HESSU_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"

# Option B: Dynamic key fetching from Sentinel
# SENTINEL_URL=https://sentinel.hessutech.com

# Optional: Key cache duration (milliseconds)
KEY_CACHE_DURATION=3600000

# Server port
PORT=3001

Configuration Options

interface HessuAuthConfig {
  // Static public key (PEM format) - recommended
  publicKey?: string;
  
  // Sentinel URL for dynamic key fetching
  sentinelUrl?: string;
  
  // Expected app slug (validates token's 'app' claim)
  expectedApp?: string;
  
  // Cache duration for fetched keys (default: 1 hour)
  keyCacheDuration?: number;
}

Architecture

How It Works

  1. Client Authentication:

    • Flutter/Web/Mobile client authenticates with Supabase
    • Client calls Sentinel's /api/auth/exchange with Supabase JWT
    • Sentinel validates Supabase JWT and issues Hessu-JWT
  2. Service Validation (This Library):

    • Client sends Hessu-JWT to Service B/C via Authorization: Bearer <token>
    • hessu-auth-consumer verifies JWT signature using RS256 public key
    • If valid, request.user is populated with decoded payload
    • Service uses request.user.sub and request.user.roles for authorization

JWT Payload Structure

interface HessuUserContext {
  sub: string;        // User ID (UUID)
  app: string;        // Application slug (e.g., 'service-b')
  roles: string[];    // User roles for this app (e.g., ['admin', 'user'])
  exp: number;        // Expiration timestamp
  iat: number;        // Issued at timestamp
}

Security Model

  • RS256 Algorithm: Asymmetric cryptography (private key signs, public key verifies)
  • No Database Dependency: Services don't need direct database access
  • 15-Minute Expiration: Tokens expire quickly to minimize security window
  • App Claim Validation: Tokens are scoped to specific services
  • Issuer Validation: Only tokens from 'hessu-sentinel' are accepted

Usage Examples

Example 1: Protected Routes with Role Checking

await server.register(async (protectedRoutes) => {
  await protectedRoutes.register(hessuAuthPlugin, {
    publicKey: process.env.HESSU_PUBLIC_KEY,
    expectedApp: 'service-b'
  });

  // Admin-only endpoint
  protectedRoutes.get('/api/admin/stats', async (request, reply) => {
    if (!request.user.roles.includes('admin')) {
      return reply.code(403).send({
        error: 'FORBIDDEN',
        message: 'Admin role required'
      });
    }

    return { stats: { ... } };
  });

  // User-specific data (ownership check)
  protectedRoutes.get('/api/users/:userId', async (request, reply) => {
    const { userId } = request.params;
    
    const isOwner = request.user.sub === userId;
    const isAdmin = request.user.roles.includes('admin');

    if (!isOwner && !isAdmin) {
      return reply.code(403).send({
        error: 'FORBIDDEN',
        message: 'You can only access your own data'
      });
    }

    return { data: { ... } };
  });
});

Example 2: Dynamic Key Fetching

await server.register(hessuAuthPlugin, {
  sentinelUrl: 'https://sentinel.hessutech.com',
  expectedApp: 'service-c',
  keyCacheDuration: 3600000 // 1 hour
});

Example 3: Express Integration

import express from 'express';
import { createVerifier } from 'hessu-auth-consumer';

const app = express();
const verifier = createVerifier({
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b'
});

// Authentication middleware
app.use(async (req, res, next) => {
  try {
    const token = verifier.extractToken(req.headers.authorization);
    const user = await verifier.verify(token);
    req.user = user;
    next();
  } catch (error) {
    res.status(error.statusCode).json({
      error: error.type,
      message: error.message
    });
  }
});

app.get('/api/protected', (req, res) => {
  res.json({ userId: req.user.sub });
});

Error Handling

Error Types

enum HessuAuthError {
  MISSING_TOKEN = 'MISSING_TOKEN',       // 401: No Authorization header
  INVALID_TOKEN = 'INVALID_TOKEN',       // 401: Malformed token
  EXPIRED_TOKEN = 'EXPIRED_TOKEN',       // 401: Token expired
  INVALID_SIGNATURE = 'INVALID_SIGNATURE', // 401: Signature verification failed
  APP_MISMATCH = 'APP_MISMATCH',         // 403: Token for different app
  INVALID_CONFIG = 'INVALID_CONFIG',     // 500: Configuration error
  KEY_FETCH_FAILED = 'KEY_FETCH_FAILED'  // 500: Failed to fetch public key
}

Error Response Format

{
  "error": "EXPIRED_TOKEN",
  "message": "Token has expired",
  "statusCode": 401,
  "timestamp": "2026-01-04T12:34:56.789Z"
}

API Reference

hessuAuthPlugin

Fastify plugin for JWT authentication.

Type: FastifyPluginAsync<HessuAuthPluginOptions>

Usage:

await server.register(hessuAuthPlugin, {
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b'
});

Behavior:

  • Adds preHandler hook to all routes in scope
  • Extracts token from Authorization: Bearer <token> header
  • Verifies token signature and expiration
  • Populates request.user with decoded payload
  • Returns 401/403 for invalid tokens

createVerifier(config)

Creates a standalone JWT verifier (for non-Fastify usage).

Parameters:

  • config: HessuAuthConfig - Verification configuration

Returns: HessuJwtVerifier

Methods:

  • verify(token: string): Promise<HessuUserContext> - Verify and decode token
  • extractToken(authHeader: string | undefined): string - Extract Bearer token

Usage:

const verifier = createVerifier({
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b'
});

const token = verifier.extractToken(authHeader);
const user = await verifier.verify(token);

Testing

Unit Testing Your Service

import { createVerifier } from 'hessu-auth-consumer';

describe('Authentication', () => {
  const verifier = createVerifier({
    publicKey: TEST_PUBLIC_KEY,
    expectedApp: 'service-b'
  });

  it('should verify valid token', async () => {
    const token = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...';
    const user = await verifier.verify(token);
    expect(user.sub).toBe('user-123');
  });

  it('should reject expired token', async () => {
    const expiredToken = '...';
    await expect(verifier.verify(expiredToken))
      .rejects.toThrow('Token has expired');
  });
});

Best Practices

1. Use Static Public Key in Production

// ✅ Good: Static key (no external dependencies)
await server.register(hessuAuthPlugin, {
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b'
});

// ❌ Avoid in production: Dynamic fetching (network dependency)
await server.register(hessuAuthPlugin, {
  sentinelUrl: 'https://sentinel.hessutech.com',
  expectedApp: 'service-b'
});

2. Always Set expectedApp

// ✅ Good: Validates app claim (prevents cross-service token reuse)
await server.register(hessuAuthPlugin, {
  publicKey: process.env.HESSU_PUBLIC_KEY,
  expectedApp: 'service-b' // 👈 Critical for security
});

3. Handle Token Expiration Gracefully

// Frontend should refresh token before expiration
if (tokenExpiresIn < 60) { // Less than 1 minute
  await refreshToken();
}

4. Separate Public and Protected Routes

// Public routes (no auth)
server.get('/health', async () => ({ status: 'ok' }));
server.get('/docs', async () => ({ ... }));

// Protected routes (auth required)
await server.register(async (protectedRoutes) => {
  await protectedRoutes.register(hessuAuthPlugin, { ... });
  
  protectedRoutes.get('/api/me', async (request) => {
    return { userId: request.user.sub };
  });
});

Troubleshooting

Error: "Invalid configuration: Must provide either publicKey or sentinelUrl"

Solution: Set HESSU_PUBLIC_KEY or SENTINEL_URL in your environment variables.

export HESSU_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n..."

Error: "Token signature verification failed"

Causes:

  1. Public key mismatch (using wrong key)
  2. Token signed by different Sentinel instance
  3. Corrupted token

Solution: Verify you're using the correct public key from Sentinel.


Error: "Token is for app 'service-c', but this service expects 'service-b'"

Cause: Token was issued for a different service.

Solution: Client must call /api/auth/exchange with correct target_app.

// Client request
const response = await fetch('https://sentinel.example.com/api/auth/exchange', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${supabaseToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ target_app: 'service-b' }) // 👈 Must match
});

Integration Checklist

  • Install hessu-auth-consumer in your service
  • Add HESSU_PUBLIC_KEY to environment variables
  • Set SERVICE_APP_ID to match your service slug
  • Register hessuAuthPlugin on protected routes
  • Access request.user in route handlers
  • Implement role-based authorization (check request.user.roles)
  • Test with valid and invalid tokens
  • Handle 401/403 errors on frontend
  • Set up token refresh flow (before 15-minute expiration)

License

MIT


Support

For issues or questions, contact the Hessutech team or open an issue on GitHub.


Version: 1.0.0
Last Updated: January 4, 2026
Maintained By: Hessutech Engineering

Dependencies

Dependencies

ID Version
dotenv ^16.4.7
jose ^5.9.6

Development Dependencies

ID Version
@types/node ^22.10.5
fastify ^5.6.2
typescript ^5.9.3

Peer Dependencies

ID Version
fastify ^5.0.0

Keywords

jwt authentication fastify rs256 hessutech middleware
Details
npm
2026-01-04 12:50:21 +00:00
0
Hessutech
MIT
15 KiB
Assets (1)
Versions (4) View all
1.2.1 2026-01-04
1.2.0 2026-01-04
1.1.0 2026-01-04
1.0.0 2026-01-04