Express TypeScript authentication/authorization middleware for Cakemail's API. This middleware verifies JWT Bearer tokens, authorizes access to impersonated accounts, and provides user/account data to downstream handlers.
- JWT Bearer token verification using RSA public key
- Account impersonation authorization via API calls
- Automatic user data loading from
/users/self - Redis caching to minimize API calls
- Fail-open caching (continues without cache if Redis unavailable)
- Full TypeScript support with strict typing
- Non-intrusive data storage using Express
res.locals - Dual package support (CommonJS + ESM)
npm install @cakemail-org/ngapi-ts-auth-middlewareimport express from 'express';
import { createAuthMiddleware } from '@cakemail-org/ngapi-ts-auth-middleware';
const app = express();
// Public key is automatically fetched from {API_BASE_URL}/token/pubkey
const authMiddleware = createAuthMiddleware({
cacheSecret: process.env.CACHE_SECRET, // Required: Secret for HMAC and encryption
enableCaching: true,
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
db: parseInt(process.env.REDIS_DB || '0'),
},
});
// Apply to all routes
app.use(authMiddleware);
// Or apply to specific routes
app.get('/api/resource', authMiddleware, (req, res) => {
res.json({
userId: res.locals.user.id,
userEmail: res.locals.user.email,
userAccountId: res.locals.user.account.id,
targetAccountId: res.locals.account.id,
});
});
app.listen(3000);| Option | Type | Required | Default | Description |
|---|---|---|---|---|
cacheSecret |
string |
Yes | - | Required secret for HMAC cache keys and Redis data encryption. Must be a strong, random value. Keep this secret secure! |
publicKey |
`string | Buffer` | No | Auto-fetched from {API_BASE_URL}/token/pubkey |
apiBaseUrl |
string |
No | process.env.CAKEMAILAPI_BASE_URL or https://api.cakemail.dev |
API base URL |
enableCaching |
boolean |
No | true |
Enable Redis caching |
redis |
RedisConfig |
No | - | Redis connection configuration |
accountIdParams |
string[] |
No | ['accountId', 'account_id'] |
Query parameter names for account ID |
onError |
(error, req) => void |
No | - | Custom error handler |
jwtOptions |
JwtOptions |
No | - | JWT verification options |
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
host |
string |
No | process.env.REDIS_HOST or localhost |
Redis host |
port |
number |
No | process.env.REDIS_PORT or 6379 |
Redis port |
db |
number |
No | process.env.REDIS_DB or 0 |
Redis database number |
password |
string |
No | process.env.REDIS_PASSWORD |
Redis password |
keyPrefix |
string |
No | ngapi: |
Redis key prefix |
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
algorithms |
string[] |
No | ['RS256'] |
Allowed JWT algorithms |
issuer |
string |
No | urn:cakemail |
Expected JWT issuer |
clockTolerance |
number |
No | 10 |
Clock tolerance in seconds |
The middleware respects the following environment variables:
CACHE_SECRET: Required - Secret for HMAC and Redis encryption (generate withopenssl rand -base64 32)CAKEMAILAPI_BASE_URL: API base URL (default:https://api.cakemail.dev)REDIS_HOST: Redis host (default:localhost)REDIS_PORT: Redis port (default:6379)REDIS_DB: Redis database (default:0)REDIS_PASSWORD: Redis password (optional)
The middleware stores authentication data in res.locals, following Express best practices for passing data between middleware:
Contains the authenticated user's data from /users/self and JWT claims:
{
id: string;
email: string;
first_name: string;
last_name: string;
account: Account; // User's own account (from JWT)
scopes: string[]; // User's scopes
user_key: string; // User's API key
// ... other user properties
}Contains the target account data:
- If
?accountId=Xquery parameter is present: authorized impersonated account - If no query parameter: user's own account (same as
res.locals.user.account)
{
id: string;
name: string;
lineage: string;
status: string;
usage_limits: UsageLimits;
// ... other account properties
}The raw JWT Bearer token string.
The middleware guarantees the following:
res.locals.account.id: Always the target account ID (safe to use for operations)res.locals.user.account.id: Always the user's own account ID (never changes with impersonation)
This ensures downstream handlers always know:
- Which account is being operated on (
res.locals.account.id) - Which user is making the request (
res.locals.user.id) - Which account the user belongs to (
res.locals.user.account.id)
When a query parameter accountId or account_id is present, the middleware:
- Verifies the JWT token
- Calls
GET /accounts/:accountIdwith the Bearer token - If returns 200: access is authorized,
res.locals.accountis populated with account data - If returns 403/401: throws
AuthorizationError(403 response)
Example:
// User with account 1627783 accessing account 999999
GET /api/resource?accountId=999999
Authorization: Bearer <token>
// Result:
// res.locals.user.account.id = "1627783" (user's own account)
// res.locals.account.id = "999999" (target account)The middleware caches API responses in Redis to minimize API calls:
ngapi:{tokenHash}:{accountId|userId}:{type}
tokenHash: First 16 characters of SHA256(token)accountId|userId: Account or user IDtype:accountoruser
Example: ngapi:a3f2c8d1e5f7:1627783:account
- Cache keys expire when the JWT token expires
- Min TTL: 60 seconds
- Max TTL: 24 hours
If Redis is unavailable:
- Logs warning to console
- Continues without caching
- Makes API calls on every request
This ensures authentication/authorization remains functional even if Redis is down (at the cost of performance).
The middleware returns the following HTTP error responses:
- Missing Authorization header
- Invalid token format
- Expired token
- Invalid token signature
Response:
{
"error": "Authentication failed",
"message": "Token has expired"
}- User does not have access to requested account
Response:
{
"error": "Authorization failed",
"message": "Access denied to account 999999"
}- Unexpected errors during authentication/authorization
Response:
{
"error": "Internal server error",
"message": "An unexpected error occurred during authentication"
}You can provide a custom error handler for logging or monitoring:
const authMiddleware = createAuthMiddleware({
publicKey,
onError: (error, req) => {
console.error('Auth error:', {
error: error.message,
path: req.path,
method: req.method,
});
},
});import express from 'express';
import { createAuthMiddleware } from '@cakemail-org/ngapi-ts-auth-middleware';
const app = express();
// Public key is automatically fetched from the API
const authMiddleware = createAuthMiddleware({
// Optional: specify API base URL (defaults to CAKEMAILAPI_BASE_URL env var)
// apiBaseUrl: 'https://api.cakemail.dev',
});
app.use(authMiddleware);
app.get('/api/campaigns', (req, res) => {
// Access authenticated user
console.log(`User ${res.locals.user.email} accessing account ${res.locals.account.id}`);
res.json({ campaigns: [] });
});
app.listen(3000);const authMiddleware = createAuthMiddleware({
enableCaching: true,
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
db: parseInt(process.env.REDIS_DB || '0'),
password: process.env.REDIS_PASSWORD,
},
});const authMiddleware = createAuthMiddleware({
// Public key is auto-fetched, but you can provide it manually if needed
// publicKey: fs.readFileSync('./pubkey.pem'),
apiBaseUrl: process.env.CAKEMAILAPI_BASE_URL,
enableCaching: true,
accountIdParams: ['accountId', 'account_id', 'aid'],
redis: {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
keyPrefix: 'myapp:',
},
onError: (error, req) => {
console.error('Auth error:', error, 'Path:', req.path);
},
jwtOptions: {
algorithms: ['RS256'],
issuer: 'urn:cakemail',
clockTolerance: 30,
},
});import { createAuthMiddleware } from '@cakemail-org/ngapi-ts-auth-middleware';
const authMiddleware = createAuthMiddleware({});
// Public routes (no auth)
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// Protected routes
app.get('/api/*', authMiddleware);
app.get('/api/campaigns', (req, res) => {
// res.locals.user and res.locals.account are guaranteed to exist here
res.json({ campaigns: [] });
});The middleware is written in TypeScript with full type definitions. It automatically augments Express.Locals so TypeScript knows about res.locals.user, res.locals.account, and res.locals.token without any manual type declarations.
When you import this package, Express.Locals is automatically augmented:
import express from 'express';
import { createAuthMiddleware } from '@cakemail/ngapi-ts-auth-middleware';
const app = express();
const authMiddleware = createAuthMiddleware({ cacheSecret: process.env.CACHE_SECRET });
app.get('/api/resource', authMiddleware, (req, res) => {
// TypeScript automatically knows about these types:
// - res.locals.user is AuthenticatedUser | undefined
// - res.locals.account is Account | undefined
// - res.locals.token is string | undefined
if (!res.locals.user || !res.locals.account) {
return res.status(500).json({ error: 'Authentication data missing' });
}
res.json({
userId: res.locals.user.id,
userEmail: res.locals.user.email,
accountId: res.locals.account.id,
});
});No manual type casting or custom type declarations required.
Types can be imported directly from the package for use in your application:
import {
AuthMiddlewareConfig,
AuthenticatedUser,
Account,
User,
JwtPayload,
AuthenticationError,
AuthorizationError,
} from '@cakemail-org/ngapi-ts-auth-middleware';Run tests:
npm testRun tests with coverage:
npm run test:coverage- HTTPS Only: Always use HTTPS in production
- Public Key Security: Never expose or commit private keys
- Token Expiration: Tokens should have reasonable expiration times
- Cache Key Hashing: Tokens are hashed in cache keys to prevent leakage
- Error Messages: Error messages don't leak sensitive token information
- Redis Caching: Reduces API calls by 90%+ for repeated requests
- Connection Pooling: ioredis and axios handle connection pooling automatically
- Lazy Initialization: Services initialize only when first needed
- Async Operations: User and account data fetched in parallel when possible
MIT
Contributions are welcome! Please submit pull requests to the GitHub repository.
For issues or questions, please file an issue on GitHub.