@abstraks-dev/api-key-auth
SSM-based API key authentication with caching and timing-safe comparison for AWS Lambda functions.
Features
- ✅ SSM Parameter Store Integration - Securely fetch API keys from AWS Systems Manager
- ✅ Automatic Caching - Cache API keys with configurable TTL (default: 5 minutes)
- ✅ Timing-Safe Comparison - Prevent timing attacks with constant-time string comparison
- ✅ Lambda Middleware - Easy-to-use middleware for automatic authentication
- ✅ Multiple Header Support - Support for lowercase and uppercase header names
- ✅ Comprehensive Error Handling - Proper error codes and messages
- ✅ Zero Configuration - Works out of the box with sensible defaults
Installation
npm install @abstraks-dev/api-key-auth @aws-sdk/client-ssm
Quick Start
Basic Usage
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
export const handler = async (event) => {
try {
await validator.validateApiKey(event);
return {
statusCode: 200,
body: JSON.stringify({ message: 'Success' }),
};
} catch (error) {
return {
statusCode: error.statusCode || 500,
body: JSON.stringify({ error: error.message }),
};
}
};
Using Middleware
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
const myHandler = async (event, context, callback) => {
return { statusCode: 200, body: 'Success' };
};
export const handler = validator.middleware(myHandler);
API Reference
createApiKeyValidator(parameterName, options)
Factory function to create a new API key validator.
Parameters:
parameterName (string, required): SSM parameter name (e.g., /abstraks/service/dev/apiKey)
options (object, optional):
region (string): AWS region (defaults to process.env.AWS_REGION or 'us-west-2')
cacheTTL (number): Cache TTL in milliseconds (defaults to 300000 = 5 minutes)
Returns: ApiKeyValidator instance
Example:
const validator = createApiKeyValidator('/abstraks/query/prod/apiKey', {
region: 'us-east-1',
cacheTTL: 10 * 60 * 1000,
});
ApiKeyValidator Class
Constructor
new ApiKeyValidator(options);
Options:
parameterName (string, required): SSM parameter name
region (string): AWS region
cacheTTL (number): Cache TTL in milliseconds
Methods
validateApiKey(event, options)
Validates API key from Lambda event headers.
Parameters:
event (object, required): Lambda event object
options (object, optional):
headerName (string): Header name to check (defaults to 'x-api-key')
Returns: Promise<boolean> - Returns true if valid
Throws: Error with statusCode property:
401: Missing or invalid API key
500: Service configuration error
Example:
try {
await validator.validateApiKey(event);
console.log('API key is valid');
} catch (error) {
console.error(`Validation failed: ${error.message}`);
}
getExpectedApiKey()
Fetches API key from SSM Parameter Store (with caching).
Returns: Promise<string> - The API key value
Throws: Error if parameter not found or SSM fetch fails
Example:
const apiKey = await validator.getExpectedApiKey();
timingSafeEqual(a, b)
Performs timing-safe string comparison.
Parameters:
a (string): First string
b (string): Second string
Returns: boolean - True if strings are equal
Example:
const isMatch = validator.timingSafeEqual('key1', 'key2');
middleware(handler)
Creates Lambda middleware for automatic authentication.
Parameters:
handler (Function): Lambda handler function to wrap
Returns: Function - Wrapped handler function with automatic API key validation
Example:
const wrappedHandler = validator.middleware(myHandler);
clearCache()
Clears cached API key (forces fresh fetch on next validation).
Example:
validator.clearCache();
isCacheValid()
Checks if cached API key is still valid.
Returns: boolean - True if cache is valid
Example:
if (!validator.isCacheValid()) {
console.log('Cache expired');
}
getCacheStats()
Gets cache statistics.
Returns: Object with:
hasCachedKey (boolean): Whether key is cached
cacheAge (number|null): Age of cache in milliseconds
cacheTTL (number): Cache TTL in milliseconds
isValid (boolean): Whether cache is valid
Example:
const stats = validator.getCacheStats();
console.log(`Cache age: ${stats.cacheAge}ms`);
Usage Patterns
Pattern 1: Basic Validation
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
export const handler = async (event) => {
try {
await validator.validateApiKey(event);
} catch (error) {
return {
statusCode: error.statusCode,
body: JSON.stringify({ error: error.message }),
};
}
};
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
export const handler = async (event) => {
try {
await validator.validateApiKey(event, { headerName: 'x-custom-api-key' });
} catch (error) {
}
};
Pattern 3: Middleware with Error Handling
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
import {
createErrorResponse,
createSuccessResponse,
} from '@abstraks/lambda-responses';
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
const myHandler = async (event, context, callback) => {
const data = await processRequest(event);
return callback(null, createSuccessResponse(data));
};
export const handler = validator.middleware(myHandler);
Pattern 4: Multi-Service Setup
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
export const queryApiKeyValidator = createApiKeyValidator(
process.env.QUERY_API_KEY_PARAM
);
import { queryApiKeyValidator } from '../helpers/apiKeyAuth.js';
export const handler = async (event) => {
await queryApiKeyValidator.validateApiKey(event);
};
Integration with CDK
IAM Permissions
Your Lambda function needs permissions to read from SSM Parameter Store:
eventDataLambda.addToRolePolicy(
new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [
`arn:aws:ssm:${this.region}:${this.account}:parameter${apiKeyParam}`,
],
conditions: {
StringEquals: {
'ssm:ParameterType': 'SecureString',
},
},
})
);
eventDataLambda.addToRolePolicy(
new iam.PolicyStatement({
actions: ['kms:Decrypt'],
resources: ['arn:aws:kms:*:*:key/*'],
})
);
Environment Variables
const lambdaEnv = {
API_KEY_PARAM: `/abstraks/query/${environment}/apiKey`,
};
Security Features
Timing-Safe Comparison
The package uses crypto.timingSafeEqual() to prevent timing attacks:
if (apiKey === expectedKey) { ... }
if (validator.timingSafeEqual(apiKey, expectedKey)) { ... }
Cache TTL for Key Rotation
Keys are automatically refetched after TTL expires, supporting key rotation:
const validator = createApiKeyValidator('/api-key', {
cacheTTL: 5 * 60 * 1000,
});
SecureString Support
The package uses WithDecryption: true to automatically decrypt SecureString parameters from SSM.
Error Handling
All errors include a statusCode property for proper HTTP responses:
try {
await validator.validateApiKey(event);
} catch (error) {
console.error(error.message);
console.log(error.statusCode);
}
Error Types:
401 Unauthorized: Missing or invalid API key
500 Internal Server Error: SSM configuration error
Testing
Mocking in Tests
import { jest } from '@jest/globals';
const mockSend = jest.fn();
jest.unstable_mockModule('@aws-sdk/client-ssm', () => ({
SSMClient: jest.fn(() => ({ send: mockSend })),
GetParameterCommand: jest.fn(),
}));
mockSend.mockResolvedValue({
Parameter: { Value: 'test-api-key' },
});
Example Test
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
test('should validate correct API key', async () => {
const validator = createApiKeyValidator('/test/api-key');
const event = {
headers: { 'x-api-key': 'correct-key' },
};
await expect(validator.validateApiKey(event)).resolves.toBe(true);
});
Migration from Service-Specific Code
Before (Duplicated in every service)
import crypto from 'crypto';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
const ssmClient = new SSMClient({ region: process.env.AWS_REGION });
let cachedApiKey = null;
async function getExpectedApiKey() {
if (cachedApiKey) return cachedApiKey;
}
function timingSafeEqual(a, b) {
}
export const handler = async (event) => {
const apiKey = event.headers['x-api-key'];
const expected = await getExpectedApiKey();
if (!timingSafeEqual(apiKey, expected)) {
return { statusCode: 401, body: 'Unauthorized' };
}
};
After (Using shared module)
import { createApiKeyValidator } from '@abstraks-dev/api-key-auth';
const validator = createApiKeyValidator(process.env.API_KEY_PARAM);
const myHandler = async (event) => {
};
export const handler = validator.middleware(myHandler);
Result: Eliminated 50+ lines of duplicated code per service! ✨
Performance
- First Request: ~100-200ms (SSM fetch)
- Cached Requests: <1ms (in-memory lookup)
- Memory: <1KB per validator instance
License
MIT
Related Packages