hessutech-auth-consumer (1.2.1)
Installation
registry=npm install hessutech-auth-consumer@1.2.1"hessutech-auth-consumer": "1.2.1"About this package
hessutech-auth-consumer
Reusable JWT authentication middleware for Hessutech services
Purpose
Standardizes authentication across all Hessutech services (Service B, Service C, etc.) by providing a turnkey Fastify plugin that validates Hessu-JWT tokens issued by The Sentinel. Uses RS256 asymmetric cryptography for secure, scalable token verification without shared secrets.
Features
- RS256 JWT Verification: Asymmetric public key cryptography (no shared secrets)
- Fastify Plugin: Drop-in authentication with automatic
request.userdecoration - App Claim Validation: Prevents cross-service token reuse
- TypeScript-First: Full type safety with strict typing
- Flexible Key Management: Static public key or dynamic fetching from Sentinel
- Zero Configuration: Works out of the box with sensible defaults
Installation
1. Configure NPM Registry
Create or edit .npmrc in your project root:
@hessutech:registry=https://reg.hessutech.fi/api/packages/admGitea/npm/
//reg.hessutech.fi/api/packages/admGitea/npm/:_authToken=${GITEA_NPM_TOKEN}
Set your authentication token:
export GITEA_NPM_TOKEN="your-gitea-access-token"
2. Install Package
npm install hessutech-auth-consumer
Configuration
Environment Variables
Create a .env file in your service:
# Required: Sentinel URL for dynamic key fetching (recommended)
SENTINEL_URL=https://sentinel.hessutech.fi
# Optional: Static public key (alternative to dynamic fetching)
HESSU_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
-----END PUBLIC KEY-----"
# Required: Your application identifier
SERVICE_APP_ID=service-b
# Optional: Server port
PORT=3001
Configuration Options
| Variable | Required | Description |
|---|---|---|
SENTINEL_URL |
Yes (if no static key) | Base URL of The Sentinel service |
HESSU_PUBLIC_KEY |
Yes (if no Sentinel URL) | PEM-encoded RS256 public key |
SERVICE_APP_ID |
Yes | Application slug (e.g., service-b, service-c) |
Recommended: Use SENTINEL_URL for automatic key rotation support. Static keys require manual updates when rotated.
Usage
Basic Setup
import Fastify from 'fastify';
import { hessuAuthPlugin } from 'hessutech-auth-consumer';
const server = Fastify({ logger: true });
// Public routes (no authentication)
server.get('/health', async () => {
return { status: 'ok' };
});
// Protected routes (authentication required)
await server.register(async (protectedRoutes) => {
await protectedRoutes.register(hessuAuthPlugin, {
sentinelUrl: process.env.SENTINEL_URL,
expectedApp: process.env.SERVICE_APP_ID,
});
protectedRoutes.get('/api/me', async (request) => {
// request.user is automatically populated
return {
userId: request.user.sub,
app: request.user.app,
roles: request.user.roles,
};
});
});
await server.listen({ port: 3001, host: '0.0.0.0' });
Role-Based Access Control
protectedRoutes.get('/api/admin/stats', async (request, reply) => {
// Check user role
if (!request.user.roles.includes('admin')) {
return reply.code(403).send({
error: 'FORBIDDEN',
message: 'Admin role required',
});
}
return { stats: { totalUsers: 1234 } };
});
Multiple Protected Scopes
// Service B routes
await server.register(async (serviceBRoutes) => {
await serviceBRoutes.register(hessuAuthPlugin, {
sentinelUrl: process.env.SENTINEL_URL,
expectedApp: 'service-b',
});
serviceBRoutes.get('/api/service-b/data', async (request) => {
return { data: 'Service B data', user: request.user.sub };
});
});
// Service C routes (different app ID)
await server.register(async (serviceCRoutes) => {
await serviceCRoutes.register(hessuAuthPlugin, {
sentinelUrl: process.env.SENTINEL_URL,
expectedApp: 'service-c',
});
serviceCRoutes.get('/api/service-c/data', async (request) => {
return { data: 'Service C data', user: request.user.sub };
});
});
Static Public Key (Alternative)
await protectedRoutes.register(hessuAuthPlugin, {
publicKey: process.env.HESSU_PUBLIC_KEY,
expectedApp: process.env.SERVICE_APP_ID,
});
Request User Payload
After successful authentication, request.user contains:
interface HessuUserContext {
sub: string; // User ID (UUID from Supabase)
app: string; // Application slug (e.g., 'service-b')
roles: string[]; // User roles for this app (e.g., ['admin', 'user'])
exp: number; // Token expiration (Unix timestamp)
iat: number; // Token issued at (Unix timestamp)
}
Example Payload
{
sub: "550e8400-e29b-41d4-a716-446655440000",
app: "service-b",
roles: ["user", "admin"],
exp: 1735689600,
iat: 1735603200
}
How Authentication Works
- Client requests token: Frontend calls Sentinel's
/api/auth/exchangewith Supabase JWT - Sentinel issues Hessu-JWT: Signed with RS256 private key, valid for 24 hours
- Client calls your service: Includes
Authorization: Bearer <hessu-jwt>header - Plugin validates token:
- Verifies RS256 signature using public key
- Checks expiration and issuer
- Validates app claim matches
expectedApp - Populates
request.userwith decoded payload
- Route handler executes: Access user context via
request.user
Error Responses
401 Unauthorized
Token is missing, invalid, expired, or signature verification failed.
{
"statusCode": 401,
"error": "Unauthorized",
"message": "Invalid or expired Hessu-JWT token"
}
403 Forbidden
Token is valid but app claim does not match expected app.
{
"statusCode": 403,
"error": "Forbidden",
"message": "Token not valid for this service"
}
Advanced Configuration
Custom Key Cache Duration
await protectedRoutes.register(hessuAuthPlugin, {
sentinelUrl: process.env.SENTINEL_URL,
expectedApp: process.env.SERVICE_APP_ID,
keyCacheDuration: 7200, // Cache for 2 hours (default: 3600)
});
Standalone JWT Verifier
Use the verifier directly without Fastify plugin:
import { createVerifier } from 'hessutech-auth-consumer';
const verifier = createVerifier({
publicKey: process.env.HESSU_PUBLIC_KEY,
expectedApp: 'service-b',
});
try {
const payload = await verifier.verify(token);
console.log('User ID:', payload.sub);
console.log('Roles:', payload.roles);
} catch (error) {
console.error('Token validation failed:', error.message);
}
TypeScript Support
Full TypeScript support with type inference:
import type { HessuUserContext } from 'hessutech-auth-consumer';
// Type-safe route handler
protectedRoutes.get('/api/profile', async (request) => {
const user: HessuUserContext = request.user;
return {
id: user.sub,
roles: user.roles,
};
});
Security Considerations
- Always use HTTPS in production: Tokens transmitted over HTTP can be intercepted
- Validate app claim: Prevents tokens issued for Service B from being used on Service C
- Short-lived tokens: Hessu-JWT tokens expire in 24 hours
- Key rotation: Use
SENTINEL_URLfor automatic key rotation support - No shared secrets: RS256 means services only need the public key
Troubleshooting
"Invalid or expired token"
- Verify token is not expired (check
expclaim) - Ensure public key matches Sentinel's signing key
- Check token was issued by Sentinel (issuer must be
hessu-sentinel)
"Token not valid for this service"
- Ensure
expectedAppmatches theappclaim in the token - Verify token was requested with correct
targetAppduring exchange
"Failed to fetch public key"
- Verify
SENTINEL_URLis correct and accessible - Check network connectivity to Sentinel
- Ensure Sentinel's
/api/auth/keysendpoint is responding
License
MIT
Package Information
- Name:
hessutech-auth-consumer - Version: 1.0.0
- Registry: https://reg.hessutech.fi/api/packages/admGitea/npm/
- Author: Hessutech
- Node.js: >=24.0.0
Support
For issues or questions, contact the Hessutech development team or file an issue in the Gitea repository.
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 |