Production-grade internal microservice authentication using signed JWT request envelopes.
Service Guard Stack provides a lightweight authentication layer for service-to-service communication by signing requests with HMAC SHA-256, validating trusted issuers, verifying request integrity, and optionally preventing replay attacks.
-
🔐 HMAC SHA-256 signed JWT envelopes
-
🏢 Service-to-service authentication
-
🔒 Request binding using HTTP method, path, and body hash
-
⏱️ Short-lived tokens with expiration validation
-
🧾 Issuer (
iss) and audience (aud) validation -
🔄 Optional replay protection using a custom
NonceStore -
⚡ Framework adapters for:
- Express
- NestJS
- Fastify
-
📦 Zero runtime dependencies
-
🧪 Fully written in TypeScript with generated type definitions
Traditional API keys only prove who created the request. They do not guarantee that the request contents were not modified during transit.
Guard Stack signs an envelope containing request metadata:
HTTP Request
|
|
v
+------------------+
| JWT Envelope |
|------------------|
| iss |
| aud |
| iat |
| exp |
| jti |
| method |
| path |
| bodyHash |
+------------------+
|
|
v
HMAC SHA-256 Signature
The receiving service verifies:
- The sender is trusted
- The token is not expired
- The request method matches
- The request path matches
- The request body matches
- The signature is valid
- The token has not been replayed (when
NonceStoreis configured)
Using npm:
npm install @manik3112/guard-stackUsing pnpm:
pnpm add @manik3112/guard-stackUsing yarn:
yarn add @manik3112/guard-stackService A creates a signed request token before calling Service B.
import { create } from '@manik3112/guard-stack';
const body = {
name: 'John Doe',
email: 'john@example.com',
};
const token = create.execute({
issuer: 'service-a',
audience: ['service-b'],
secret: process.env.SERVICE_A_SECRET!,
method: 'POST',
path: '/users',
body,
});
await axios.post('http://service-b.url/users', body, {
headers: {
'x-serviceguard-token': token,
},
});The generated token is sent with the request:
x-serviceguard-token: <guard-stack-token>Protect incoming routes using the Express middleware.
import express from 'express';
import { expressMiddleware } from '@manik3112/guard-stack';
const app = express();
app.use(
expressMiddleware({
currentService: 'service-b',
trustedIssuers: {
'service-a': process.env.SERVICE_A_SECRET!,
},
}),
);
app.get('/users', (req, res) => {
res.json({
message: 'Authenticated request',
});
});type GuardStackMiddlewareOptions = {
currentService: string;
/**
* Map of trusted service names to their shared secrets.
*/
trustedIssuers: Record<string, string>;
/**
* Optional nonce store for replay protection.
*/
nonceStore?: NonceStore;
/**
* Header containing the token.
*
* Default: x-serviceguard-token
*/
headerName?: string;
/**
* Callback executed when validation fails.
*/
onValidationFailed?: (result: ValidationResult) => void | Promise<void>;
};By default, Guard Stack reads:
x-serviceguard-token: <token>You may use a custom header:
expressMiddleware({
currentService: 'service-b',
trustedIssuers: {
'service-a': process.env.SERVICE_A_SECRET!,
},
headerName: 'x-guard-token',
});The request should then include:
x-guard-token: <token>Replay protection is optional and can be enabled by providing a NonceStore.
Without a NonceStore, Guard Stack validates:
- Signature integrity
- Trusted issuer
- Intended audience
- Token expiration
- Request method
- Request path
- Request body hash
To prevent a valid token from being used multiple times within its lifetime, configure a NonceStore.
export interface NonceStore {
/**
* Attempts to store a nonce.
*
* Returns:
* - true => nonce was stored and the request should be accepted.
* - false => nonce already existed and the request should be rejected as a replay attack.
*
* The implementation must:
* - Store the nonce for the provided TTL.
* - Perform the operation atomically to prevent race conditions.
*/
add(jti: string, ttlSeconds: number): Promise<boolean>;
}The following example is suitable for local development or testing.
const memory = new Map<string, number>();
const nonceStore: NonceStore = {
add: async (jti, ttlSeconds) => {
if (memory.has(jti)) {
return false;
}
memory.set(jti, Date.now());
setTimeout(() => {
memory.delete(jti);
}, ttlSeconds * 1000);
return true;
},
};Use it with the middleware:
app.use(
expressMiddleware({
currentService: 'service-b',
trustedIssuers: {
'service-a': process.env.SERVICE_A_SECRET!,
},
nonceStore,
}),
);For production environments with multiple service instances, use a distributed cache such as Redis.
The operation must be atomic so that only the first request using a given jti is accepted.
Example using Redis SET NX EX:
import Redis from 'ioredis';
import { NonceStore } from '@manik3112/guard-stack';
const redis = new Redis(process.env.REDIS_URL);
const nonceStore: NonceStore = {
add: async (jti, ttlSeconds) => {
const result = await redis.set(`guard-stack:nonce:${jti}`, '1', 'EX', ttlSeconds, 'NX');
return result === 'OK';
},
};Use the provided NestJS guard to protect controllers or routes.
import { GuardStackGuard } from '@manik3112/guard-stack';
@Controller('users')
@UseGuards(GuardStackGuard)
export class UserController {
@Get()
getUsers() {
return ['John', 'Jane'];
}
}Configure the guard with the same options:
{
currentService: 'service-b',
trustedIssuers: {
'service-a': process.env.SERVICE_A_SECRET!,
},
nonceStore,
}Use the Fastify hook to validate incoming requests:
import { fastifyHook } from '@manik3112/guard-stack';
fastify.addHook(
'preHandler',
fastifyHook({
currentService: 'service-b',
trustedIssuers: {
'service-a': process.env.SERVICE_A_SECRET!,
},
nonceStore,
}),
);Every incoming request follows the following validation sequence:
Incoming Request
|
v
Extract JWT Token
|
v
Verify HMAC Signature
|
v
Validate Issuer
|
v
Validate Audience
|
v
Validate Expiration
|
v
Verify HTTP Method
|
v
Verify Request Path
|
v
Verify Body Hash
|
v
Check NonceStore (optional)
|
v
Request Accepted
Guard Stack is designed for internal service-to-service communication where services share symmetric secrets.
Shared secrets should be cryptographically random and sufficiently long.
Recommended:
32+ bytes of random data
Example:
openssl rand -hex 32Avoid:
password123
service-secret
my-api-key
Guard Stack tokens are intended to be short-lived.
Short expiration windows reduce the impact of leaked or intercepted tokens.
Recommended:
30-300 seconds
Without a NonceStore, a valid token may be reused until it expires.
For distributed environments such as Kubernetes, ECS, or multiple application instances, use a distributed store such as Redis.
Never:
- Commit secrets to Git
- Hard-code secrets into applications
- Share secrets between unrelated services
Use secret management systems such as:
- Environment variables
- Cloud secret managers
- Vault systems
When validation fails, Guard Stack returns a ValidationResult containing a failure reason.
Examples:
TOKEN_MISSING
TOKEN_INVALID
SIGNATURE_INVALID
ISSUER_UNTRUSTED
AUDIENCE_INVALID
TOKEN_EXPIRED
METHOD_MISMATCH
PATH_MISMATCH
BODY_HASH_MISMATCH
REPLAY_DETECTED
These values are useful for logging and monitoring.
In public HTTP responses, avoid exposing detailed failure reasons. A generic:
401 Unauthorizedresponse is recommended.
Guard Stack follows a few important design decisions:
The authentication and validation logic is independent of Express, NestJS, or Fastify.
Framework integrations are thin adapters around the same validation engine.
Guard Stack does not require Redis, databases, or any external services.
Replay protection is implemented through the NonceStore interface, allowing applications to choose the storage technology that fits their infrastructure.
Tokens are tied to the actual HTTP request by signing:
- HTTP method
- Normalized request path
- Deterministic body hash
A valid token for:
POST /userscannot be reused for:
DELETE /usersor a modified request body.
The package depends only on Node.js built-in cryptographic primitives.
This reduces:
- Supply chain risks
- Installation size
- Dependency maintenance overhead
src/
├── adapters/ # Framework and external integration contracts
├── core/ # Token creation and validation flows
├── crypto/ # HMAC, hashing and JWT utilities
├── middleware/ # Express, NestJS and Fastify integrations
├── types/ # Public TypeScript types
└── utils/ # Request and encoding utilities
Tokens support an optional kid (Key ID) header field.
However, kid is currently informational and is not used during validation.
Secret rotation with multiple active keys for the same issuer is planned for a future release.
Planned improvements:
- Support multiple active secrets per issuer using
kid - Additional framework integrations
- More built-in testing utilities
- Enhanced security tooling and examples
Guard Stack follows semantic versioning.
Breaking changes will only be introduced in major releases.
ISC License.
Made with ❤️ for secure service-to-service communication.