@abstraks-dev/jwt-auth
v1.0.0
Published
JWT token generation and validation with AWS Secrets Manager integration for Lambda microservices
Maintainers
Readme
@abstraks-dev/jwt-auth
JWT token generation and validation with AWS Secrets Manager integration.
Features
- 🔐 JWT Token Management: Generate and verify JSON Web Tokens
- ☁️ AWS Integration: Optional Secrets Manager for production environments
- ⚡ Secret Caching: 5-minute cache reduces AWS API calls
- 🛡️ Middleware Support: Lambda handler wrapper for automatic authentication
- 🎯 Flexible Configuration: Direct secrets, custom getSecret functions, or Secrets Manager
- ⏰ Configurable Expiration: Default 364 days, customizable per token
- 🚨 Comprehensive Error Handling: Standardized error responses with status codes
Installation
npm install @abstraks-dev/jwt-authOptional Peer Dependencies
For AWS Secrets Manager integration:
npm install @aws-sdk/client-secrets-managerUsage
Simple Usage (Direct Secret)
import { createSimpleAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = createSimpleAuthenticator(process.env.JWT_SECRET);
// Generate token
const token = await auth.generateToken({ _id: 'user123' });
// Verify token
const decoded = await auth.verifyToken(token);
console.log(decoded._id); // 'user123'Lambda Handler with Middleware
import { createSimpleAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = createSimpleAuthenticator(process.env.JWT_SECRET);
const handler = async (event) => {
// event.auth contains { userId, decoded }
const userId = event.auth.userId;
return {
statusCode: 200,
body: JSON.stringify({ message: `Hello ${userId}` }),
};
};
export const authenticatedHandler = auth.withAuth(handler);AWS Secrets Manager Integration
import { createSecretsManagerAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = await createSecretsManagerAuthenticator(
'auth-prod', // Secret name
'JWT_SECRET', // Key within secret
'us-west-2' // Region
);
const token = await auth.generateToken({ _id: 'user123' });Custom getSecret Function
import { createJWTAuthenticator } from '@abstraks-dev/jwt-auth';
import { getSecret } from './my-secret-manager.js';
const auth = createJWTAuthenticator({
getSecret,
secretName: 'auth-prod',
secretKey: 'JWT_SECRET',
});
const token = await auth.generateToken({ _id: 'user123' });Lambda Event Authentication
import { createSimpleAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = createSimpleAuthenticator(process.env.JWT_SECRET);
export const handler = async (event) => {
try {
// Extracts token from Authorization header
const { userId, decoded } = await auth.authenticateEvent(event);
return {
statusCode: 200,
body: JSON.stringify({ userId, claims: decoded }),
};
} catch (error) {
return {
statusCode: error.statusCode || 500,
body: JSON.stringify({ error: error.message }),
};
}
};API Documentation
createSimpleAuthenticator(jwtSecret)
Creates an authenticator with a direct JWT secret.
Parameters:
jwtSecret(string, required): The JWT secret key
Returns: Authenticator instance
Example:
const auth = createSimpleAuthenticator('my-secret-key-12345');createJWTAuthenticator(options)
Creates an authenticator with flexible secret management.
Parameters:
options.getSecret(function): Async function(secretName, secretKey) => stringoptions.secretName(string): Name of secret to fetchoptions.secretKey(string, optional): Key within secret (if JSON)options.jwtSecret(string): Direct JWT secret (alternative to getSecret)
Returns: Authenticator instance
Example:
const auth = createJWTAuthenticator({
getSecret: async (name, key) => {
// Your custom secret fetching logic
return mySecretManager.get(name, key);
},
secretName: 'auth-prod',
secretKey: 'JWT_SECRET',
});createSecretsManagerAuthenticator(secretName, secretKey, region)
Creates an authenticator using AWS Secrets Manager.
Parameters:
secretName(string, required): AWS Secrets Manager secret namesecretKey(string, optional): Key within JSON secretregion(string, optional): AWS region (defaults to AWS_REGION env var)
Returns: Promise
Example:
const auth = await createSecretsManagerAuthenticator(
'auth-prod',
'JWT_SECRET',
'us-west-2'
);Authenticator Methods
generateToken(payload, options)
Generates a signed JWT token.
Parameters:
payload(object, required): Token payload (typically{ _id: 'userId' })options.expiresIn(string, optional): Expiration time (default: '364d')options.additionalClaims(object, optional): Additional JWT claims
Returns: Promise - Signed JWT token
Example:
const token = await auth.generateToken(
{ _id: 'user123' },
{
expiresIn: '7d',
additionalClaims: { role: 'admin', scope: 'all' },
}
);verifyToken(token, options)
Verifies and decodes a JWT token.
Parameters:
token(string, required): JWT token to verifyoptions.requireUserId(boolean, optional): Require_idin payload (default: true)
Returns: Promise - Decoded token payload
Throws:
- Error with
statusCode: 401for invalid/expired tokens - Error messages: 'Token is required', 'Invalid token', 'Token has expired', 'Token payload invalid: user ID not found'
Example:
try {
const decoded = await auth.verifyToken(token);
console.log(decoded._id, decoded.role);
} catch (error) {
console.error(error.message); // 'Token has expired'
console.error(error.statusCode); // 401
}authenticateEvent(event, options)
Extracts and verifies token from Lambda event.
Parameters:
event(object, required): Lambda event with headersoptions.headerName(string, optional): Header name (default: 'Authorization')options.requireUserId(boolean, optional): Require_idin payload (default: true)
Returns: Promise - { userId: string, decoded: object }
Throws:
- Error with
statusCode: 401if token missing or invalid
Example:
// Handles both formats:
// Authorization: Bearer eyJhbGc...
// Authorization: eyJhbGc...
const { userId, decoded } = await auth.authenticateEvent(event);
// Custom header
const result = await auth.authenticateEvent(event, {
headerName: 'X-Auth-Token',
});withAuth(handler, options)
Middleware wrapper for Lambda handlers.
Parameters:
handler(function, required): Async Lambda handler functionoptions.headerName(string, optional): Header name (default: 'Authorization')options.requireUserId(boolean, optional): Require_idin payload (default: true)
Returns: Wrapped handler function
Behavior:
- Authenticates request before calling handler
- Attaches
event.auth = { userId, decoded }to event - Returns standardized 401 response on auth failure
- CORS headers included in error responses
Example:
const protectedHandler = async (event) => {
const userId = event.auth.userId;
const role = event.auth.decoded.role;
// Your handler logic
return {
statusCode: 200,
body: JSON.stringify({ userId, role }),
};
};
export const handler = auth.withAuth(protectedHandler);Secret Caching
The authenticator caches secrets for 5 minutes to reduce AWS API calls:
const auth = await createSecretsManagerAuthenticator('auth-prod');
await auth.generateToken({ _id: 'user1' }); // Fetches secret
await auth.generateToken({ _id: 'user2' }); // Uses cache
await auth.generateToken({ _id: 'user3' }); // Uses cache
// After 5 minutes, next call fetches fresh secretBenefits:
- Reduces AWS Secrets Manager costs
- Improves Lambda cold start performance
- Minimizes latency on subsequent requests
Considerations:
- Secret rotation takes up to 5 minutes to propagate
- Lambda container reuse extends effective cache lifetime
- Each container instance has independent cache
Error Handling
All authentication errors include a statusCode property for easy HTTP response mapping:
try {
const decoded = await auth.verifyToken(expiredToken);
} catch (error) {
console.error(error.message); // 'Token has expired'
console.error(error.statusCode); // 401
return {
statusCode: error.statusCode || 500,
body: JSON.stringify({ error: error.message }),
};
}Error Types:
Token is required- Missing token (401)Authorization token is required- Missing header in authenticateEvent (401)Invalid token- Malformed JWT (401)Token has expired- Expired token (401)Token payload invalid: user ID not found- Missing_idwhen required (401)JWT secret not configured- Configuration error (500)
Migration Guide
From Auth Service Pattern
Before (Auth service):
import { getSecret } from '../helpers/secretsManager.js';
import jwt from 'jsonwebtoken';
const generateJWTToken = async (userId) => {
const jwtSecret = await getSecret(process.env.JWT_SECRET_NAME, 'JWT_SECRET');
return jwt.sign({ _id: userId }, jwtSecret, { expiresIn: '364d' });
};
export const authenticateEvent = async (event) => {
const token =
event.headers['Authorization'] || event.headers['authorization'];
if (!token) throw new Error('Authorization token is required');
const jwtSecret = await getSecret(process.env.JWT_SECRET_NAME, 'JWT_SECRET');
const decoded = jwt.verify(token.replace('Bearer ', ''), jwtSecret);
if (!decoded._id) throw new Error('Invalid token payload');
return { userId: decoded._id, decoded };
};After (with @abstraks-dev/jwt-auth):
import { createSecretsManagerAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = await createSecretsManagerAuthenticator(
process.env.JWT_SECRET_NAME,
'JWT_SECRET',
process.env.AWS_REGION
);
// Generate token
const token = await auth.generateToken({ _id: userId });
// Authenticate event
export const handler = auth.withAuth(async (event) => {
const userId = event.auth.userId;
// Your logic here
});Benefits:
- ✅ Automatic secret caching (5 minutes)
- ✅ Standardized error handling with status codes
- ✅ Middleware pattern reduces boilerplate
- ✅ Case-insensitive header matching
- ✅ Handles both Bearer and direct token formats
- ✅ Comprehensive error messages
From Social Service Pattern
Before (Social service):
const verifyToken = async (token) => {
try {
const jwtSecret = await getSecret(secretName, 'JWT_SECRET');
return jwt.verify(token, jwtSecret);
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token has expired');
}
throw new Error('Invalid token');
}
};After:
import { createSecretsManagerAuthenticator } from '@abstraks-dev/jwt-auth';
const auth = await createSecretsManagerAuthenticator(secretName, 'JWT_SECRET');
try {
const decoded = await auth.verifyToken(token);
} catch (error) {
// error.message: 'Token has expired' or 'Invalid token'
// error.statusCode: 401
}Best Practices
1. Use Secrets Manager in Production
// Development
const auth = createSimpleAuthenticator(process.env.JWT_SECRET);
// Production
const auth = await createSecretsManagerAuthenticator(
process.env.JWT_SECRET_NAME,
'JWT_SECRET'
);2. Use Middleware for Protected Routes
// Good: Middleware handles auth automatically
export const handler = auth.withAuth(async (event) => {
const userId = event.auth.userId;
// Your logic
});
// Avoid: Manual authentication in every handler
export const handler = async (event) => {
try {
const { userId } = await auth.authenticateEvent(event);
// Your logic
} catch (error) {
return {
statusCode: 401,
body: JSON.stringify({ error: error.message }),
};
}
};3. Set Appropriate Expiration
// Long-lived user sessions
const sessionToken = await auth.generateToken(
{ _id: userId },
{ expiresIn: '364d' }
);
// Short-lived API tokens
const apiToken = await auth.generateToken(
{ _id: userId, scope: 'api' },
{ expiresIn: '1h' }
);
// Refresh tokens
const refreshToken = await auth.generateToken(
{ _id: userId, type: 'refresh' },
{ expiresIn: '30d' }
);4. Include Role/Scope in Additional Claims
const token = await auth.generateToken(
{ _id: userId },
{
additionalClaims: {
role: user.role,
permissions: user.permissions,
organizationId: user.organizationId,
},
}
);
// Later in handler
const { decoded } = event.auth;
if (decoded.role !== 'admin') {
return { statusCode: 403, body: JSON.stringify({ error: 'Forbidden' }) };
}5. Handle Errors Gracefully
export const handler = auth.withAuth(
async (event) => {
// Your logic
},
{
// Custom error handler
onError: (error) => ({
statusCode: error.statusCode || 500,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
error: error.message,
timestamp: new Date().toISOString(),
}),
}),
}
);Troubleshooting
"JWT secret not configured"
Cause: No secret provided to authenticator.
Solution:
// Ensure one of these is configured:
createJWTAuthenticator({ jwtSecret: 'secret' });
createJWTAuthenticator({ getSecret, secretName: 'name' });
await createSecretsManagerAuthenticator('name');"Authorization token is required"
Cause: Missing Authorization header in request.
Solution:
// Ensure header is present (case-insensitive):
headers: {
Authorization: 'Bearer <token>';
// OR
authorization: '<token>';
}"Token has expired"
Cause: Token expiration time has passed.
Solution: Generate new token or increase expiration:
const token = await auth.generateToken(
{ _id: userId },
{ expiresIn: '7d' } // Increase from default
);AWS Secrets Manager Errors
Issue: AccessDeniedException or timeout errors.
Solution:
- Verify Lambda has
secretsmanager:GetSecretValuepermission - Check secret exists in correct region
- Verify VPC configuration if using private subnets
- Add error handling for transient failures:
try {
const auth = await createSecretsManagerAuthenticator('auth-prod');
} catch (error) {
console.error('Failed to initialize authenticator:', error);
// Fallback or retry logic
}License
MIT
