hessutech-auth-consumer (1.0.0)
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
anytypes - ✅ 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
Option 1: Fastify Plugin (Recommended)
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
-
Client Authentication:
- Flutter/Web/Mobile client authenticates with Supabase
- Client calls Sentinel's
/api/auth/exchangewith Supabase JWT - Sentinel validates Supabase JWT and issues Hessu-JWT
-
Service Validation (This Library):
- Client sends Hessu-JWT to Service B/C via
Authorization: Bearer <token> hessu-auth-consumerverifies JWT signature using RS256 public key- If valid,
request.useris populated with decoded payload - Service uses
request.user.subandrequest.user.rolesfor authorization
- Client sends Hessu-JWT to Service B/C via
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
preHandlerhook to all routes in scope - Extracts token from
Authorization: Bearer <token>header - Verifies token signature and expiration
- Populates
request.userwith 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 tokenextractToken(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:
- Public key mismatch (using wrong key)
- Token signed by different Sentinel instance
- 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-consumerin your service - Add
HESSU_PUBLIC_KEYto environment variables - Set
SERVICE_APP_IDto match your service slug - Register
hessuAuthPluginon protected routes - Access
request.userin 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 |