gateway-cognito-auth
v1.1.0
Published
Production-ready AWS Cognito authentication package for Node.js/Express applications
Maintainers
Readme
@gateway/cognito-auth
Production-ready AWS Cognito authentication package for Node.js/Express applications with TypeScript support, multi-tenant capabilities, and comprehensive security features.
✨ Features
- 🔐 Complete AWS Cognito Integration - Full authentication flow with signup, login, and token refresh
- 🚀 Express Middleware - Drop-in JWT verification middleware for route protection
- 🏢 Multi-Tenant Support - Custom claims extraction for SaaS applications
- 📦 Dual Package Exports - CommonJS and ESM support with tree-shaking
- 🔒 Production-Safe Security - Comprehensive input validation and error handling
- 📝 Full TypeScript Support - Complete type definitions and IntelliSense
- ⚡ Performance Optimized - JWKS caching and efficient JWT verification
- 🧪 Battle-Tested - 95%+ test coverage with property-based testing
- 🐳 Container Ready - Docker/ECS compatible with zero external dependencies
📦 Installation
npm install @gateway/cognito-authPeer Dependencies
npm install express # Required for middleware functionality🚀 Quick Start
Basic Setup
import { CognitoAuthManager } from '@gateway/cognito-auth';
const authManager = new CognitoAuthManager({
userPoolId: 'us-east-1_XXXXXXXXX',
clientId: 'your-client-id',
region: 'us-east-1'
});
// Protect routes with middleware
app.use('/api/protected', authManager.authMiddleware({ tokenUse: 'id' }));Environment Variables Setup
# Required
COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
COGNITO_CLIENT_ID=your-client-id
AWS_REGION=us-east-1
# Optional (for app clients with secrets)
COGNITO_CLIENT_SECRET=your-client-secretimport { loadConfigFromEnv } from '@gateway/cognito-auth';
// Load configuration from environment variables
const config = loadConfigFromEnv();
const authManager = new CognitoAuthManager(config);📚 API Documentation
CognitoAuthManager
The main authentication class that handles all Cognito operations.
Constructor
new CognitoAuthManager(config: CognitoConfig)Parameters:
config.userPoolId(string) - AWS Cognito User Pool IDconfig.clientId(string) - AWS Cognito App Client IDconfig.clientSecret(string, optional) - App Client Secret (for confidential clients)config.region(string) - AWS region where User Pool is located
Example:
const authManager = new CognitoAuthManager({
userPoolId: 'us-east-1_XXXXXXXXX',
clientId: 'abcdef123456',
clientSecret: 'secret-for-confidential-clients', // Optional
region: 'us-east-1'
});Methods
signup(email: string, password: string): Promise<SignupResponse>
Register a new user with email and password.
try {
const result = await authManager.signup('[email protected]', 'SecurePassword123!');
console.log('User created with ID:', result.userSub);
} catch (error) {
console.error('Signup failed:', error.message);
}Returns: { userSub: string } - User's unique identifier
Throws:
ValidationError- Invalid email or password formatAuthenticationError- User already exists or Cognito service error
forgotPassword(email: string): Promise<void>
Initiate password reset flow by sending reset link to user's email.
try {
await authManager.forgotPassword('[email protected]');
console.log('Password reset link sent to email');
} catch (error) {
console.error('Password reset request failed:', error.message);
}Throws:
ValidationError- Invalid email formatAuthenticationError- User not found or service error
confirmForgotPassword(username: string, code: string, newPassword: string): Promise<void>
Confirm password reset with verification code and new password.
try {
await authManager.confirmForgotPassword('[email protected]', '123456', 'NewSecurePassword123!');
console.log('Password reset successful');
} catch (error) {
console.error('Password reset failed:', error.message);
}Throws:
ValidationError- Invalid input formatAuthenticationError- Invalid/expired code or user not found
login(email: string, password: string): Promise<AuthTokens>
Authenticate user and receive JWT tokens.
try {
const tokens = await authManager.login('[email protected]', 'SecurePassword123!');
console.log('Login successful:', {
idToken: tokens.idToken,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
});
} catch (error) {
console.error('Login failed:', error.message);
}Returns: AuthTokens object with idToken, accessToken, and refreshToken
Throws:
ValidationError- Invalid email or password formatAuthenticationError- Invalid credentials or user not confirmed
refreshToken(refreshToken: string): Promise<AuthTokens>
Refresh expired tokens using a valid refresh token.
try {
const newTokens = await authManager.refreshToken(existingRefreshToken);
console.log('Tokens refreshed successfully');
} catch (error) {
console.error('Token refresh failed:', error.message);
}Returns: AuthTokens object with new idToken, accessToken, and refreshToken
Throws:
ValidationError- Invalid refresh token formatAuthenticationError- Expired or invalid refresh token
authMiddleware(options: AuthMiddlewareOptions): RequestHandler
Create Express middleware for JWT authentication.
// Verify ID tokens (recommended for user authentication)
app.use('/api/user', authManager.authMiddleware({ tokenUse: 'id' }));
// Verify access tokens (for API access)
app.use('/api/data', authManager.authMiddleware({ tokenUse: 'access' }));
// Skip verification in development
app.use('/api/dev', authManager.authMiddleware({
tokenUse: 'id',
skipVerification: process.env.NODE_ENV === 'development'
}));Options:
tokenUse('id' | 'access') - Type of token to verifyskipVerification(boolean, optional) - Skip verification for development
Express Middleware
The middleware automatically:
- Extracts Bearer tokens from
Authorizationheaders - Verifies JWT signatures using cached JWKS
- Validates token claims (issuer, audience, expiration)
- Attaches user information to
req.user - Returns 401 for invalid/missing tokens
Accessing User Information
import { AuthenticatedRequest } from '@gateway/cognito-auth';
app.get('/api/profile', authManager.authMiddleware({ tokenUse: 'id' }),
(req: AuthenticatedRequest, res) => {
const user = req.user;
res.json({
userId: user.sub,
email: user.email,
tenantId: user.customClaims['custom:tenantId'], // Multi-tenant support
customData: user.customClaims
});
}
);Configuration Utilities
loadConfigFromEnv(env?: Record<string, string>): CognitoConfig
Load configuration from environment variables.
import { loadConfigFromEnv } from '@gateway/cognito-auth';
// Use process.env
const config = loadConfigFromEnv();
// Use custom environment object
const config = loadConfigFromEnv({
COGNITO_USER_POOL_ID: 'us-east-1_XXXXXXXXX',
COGNITO_CLIENT_ID: 'abcdef123456',
AWS_REGION: 'us-east-1'
});validateCognitoConfig(config: unknown): CognitoConfig
Validate configuration object with detailed error messages.
import { validateCognitoConfig } from '@gateway/cognito-auth';
try {
const validConfig = validateCognitoConfig({
userPoolId: 'us-east-1_XXXXXXXXX',
clientId: 'abcdef123456',
region: 'us-east-1'
});
} catch (error) {
console.error('Configuration error:', error.message);
}🔧 Configuration Reference
Environment Variables
| Variable | Required | Description | Example |
|----------|----------|-------------|---------|
| COGNITO_USER_POOL_ID | ✅ | AWS Cognito User Pool ID | us-east-1_XXXXXXXXX |
| COGNITO_CLIENT_ID | ✅ | AWS Cognito App Client ID | abcdef123456789 |
| AWS_REGION | ✅ | AWS region for User Pool | us-east-1 |
| COGNITO_CLIENT_SECRET | ❌ | App Client Secret (confidential clients only) | secret123... |
Configuration Object
interface CognitoConfig {
userPoolId: string; // Format: region_poolId
clientId: string; // Alphanumeric string
clientSecret?: string; // Optional for public clients
region: string; // Valid AWS region
}Middleware Options
interface AuthMiddlewareOptions {
tokenUse: 'id' | 'access'; // Token type to verify
skipVerification?: boolean; // Skip verification (development only)
}🏢 Multi-Tenant Support
The package automatically extracts custom claims from JWT tokens for multi-tenant applications:
app.get('/api/tenant-data', authManager.authMiddleware({ tokenUse: 'id' }),
(req: AuthenticatedRequest, res) => {
const { customClaims } = req.user;
// Access tenant-specific claims
const tenantId = customClaims['custom:tenantId'];
const role = customClaims['custom:role'];
const permissions = customClaims['custom:permissions'];
// Use tenant information for data isolation
const data = await getTenantData(tenantId);
res.json(data);
}
);Setting Custom Claims in Cognito
Custom claims must be set in your Cognito User Pool using Lambda triggers or Admin APIs:
// Example: Pre Token Generation Lambda trigger
exports.handler = async (event) => {
event.response = {
claimsOverrideDetails: {
claimsToAddOrOverride: {
'custom:tenantId': 'tenant-123',
'custom:role': 'admin',
'custom:permissions': 'read,write,delete'
}
}
};
return event;
};🔒 Security Features
Input Validation
All inputs are validated and sanitized to prevent injection attacks:
// Email validation with sanitization
const email = validateEmail(userInput.email);
// Password strength validation
const password = validatePassword(userInput.password);
// JWT token format validation
const token = validateJWTToken(authHeader);Error Handling
Production-safe error responses that don't expose sensitive information:
// Development: Detailed error messages
{
"error": "ValidationError",
"message": "Invalid email format: user@invalid",
"code": "INVALID_EMAIL"
}
// Production: Generic error messages
{
"error": "Authentication failed",
"message": "Invalid credentials provided",
"code": "AUTH_FAILED"
}JWKS Caching
Automatic caching of JSON Web Key Sets for performance and security:
- 5-minute TTL to balance security and performance
- Automatic cache invalidation
- Minimal network requests to Cognito endpoints
🧪 Testing
Running Tests
# Run all tests
npm test
# Run with coverage
npm run test:coverage
# Run LocalStack integration tests
npm run test:localstack
# Watch mode for development
npm run test:watchProperty-Based Testing
The package includes comprehensive property-based tests that verify correctness across thousands of generated inputs:
// Example: Configuration validation property test
test('Property 1: Configuration Validation Completeness', () => {
fc.assert(fc.property(
fc.record({
userPoolId: fc.string(),
clientId: fc.string(),
region: fc.string()
}),
(config) => {
// Test that valid configs pass and invalid configs fail appropriately
const result = validateCognitoConfig(config);
expect(result).toBeDefined();
}
));
});🐳 Docker Support
The package is fully compatible with containerized environments:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Environment variables for Cognito configuration
ENV COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
ENV COGNITO_CLIENT_ID=abcdef123456
ENV AWS_REGION=us-east-1
EXPOSE 3000
CMD ["npm", "start"]Docker Compose Example
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- COGNITO_USER_POOL_ID=us-east-1_XXXXXXXXX
- COGNITO_CLIENT_ID=abcdef123456
- AWS_REGION=us-east-1
- NODE_ENV=production📋 Examples
Complete Express Application
import express from 'express';
import { CognitoAuthManager, loadConfigFromEnv, AuthenticatedRequest } from '@gateway/cognito-auth';
const app = express();
app.use(express.json());
// Load configuration from environment
const config = loadConfigFromEnv();
const authManager = new CognitoAuthManager(config);
// Public routes
app.post('/auth/signup', async (req, res) => {
try {
const { email, password } = req.body;
const result = await authManager.signup(email, password);
res.json({ success: true, userSub: result.userSub });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/auth/forgot-password', async (req, res) => {
try {
const { email } = req.body;
await authManager.forgotPassword(email);
res.json({ success: true, message: 'Password reset link sent to email' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.post('/auth/reset-password', async (req, res) => {
try {
const { username, code, newPassword } = req.body;
await authManager.confirmForgotPassword(username, code, newPassword);
res.json({ success: true, message: 'Password reset successful' });
} catch (error) {
res.status(400).json({ error: error.message });
}
});
app.get('/reset-password', (req, res) => {
// Serve password reset page
res.sendFile(path.join(__dirname, 'reset-password.html'));
});
app.post('/auth/login', async (req, res) => {
try {
const { email, password } = req.body;
const tokens = await authManager.login(email, password);
res.json({ success: true, tokens });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
app.post('/auth/refresh', async (req, res) => {
try {
const { refreshToken } = req.body;
const tokens = await authManager.refreshToken(refreshToken);
res.json({ success: true, tokens });
} catch (error) {
res.status(401).json({ error: error.message });
}
});
// Protected routes
app.use('/api', authManager.authMiddleware({ tokenUse: 'id' }));
app.get('/api/profile', (req: AuthenticatedRequest, res) => {
res.json({
user: req.user,
message: 'This is a protected route'
});
});
app.get('/api/tenant-data', (req: AuthenticatedRequest, res) => {
const tenantId = req.user.customClaims['custom:tenantId'];
res.json({
tenantId,
data: `Data for tenant ${tenantId}`,
user: req.user.email
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});Error Handling Best Practices
import { CognitoAuthError, ValidationError, AuthenticationError } from '@gateway/cognito-auth';
app.post('/auth/login', async (req, res) => {
try {
const tokens = await authManager.login(req.body.email, req.body.password);
res.json({ success: true, tokens });
} catch (error) {
if (error instanceof ValidationError) {
return res.status(400).json({
error: 'Validation Error',
message: error.message,
code: error.code
});
}
if (error instanceof AuthenticationError) {
return res.status(401).json({
error: 'Authentication Error',
message: error.message,
code: error.code
});
}
// Generic error for unexpected cases
res.status(500).json({
error: 'Internal Server Error',
message: 'An unexpected error occurred'
});
}
});Custom Claims Processing
import { AuthenticatedRequest } from '@gateway/cognito-auth';
// Middleware to extract tenant context
function extractTenantContext(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const tenantId = req.user.customClaims['custom:tenantId'];
if (!tenantId) {
return res.status(403).json({ error: 'No tenant context found' });
}
// Add tenant context to request
(req as any).tenantId = tenantId;
next();
}
// Use tenant-aware middleware
app.use('/api/tenant', authManager.authMiddleware({ tokenUse: 'id' }));
app.use('/api/tenant', extractTenantContext);
app.get('/api/tenant/users', (req: any, res) => {
const tenantId = req.tenantId;
// Fetch users for specific tenant
res.json({ tenantId, users: [] });
});🔧 Development
Local Development with LocalStack
# Start LocalStack for local Cognito testing
npm run dev:setup
# Run tests against LocalStack
npm run test:localstack
# Stop LocalStack
npm run dev:stopBuilding the Package
# Clean previous builds
npm run clean
# Build all formats (CommonJS, ESM, TypeScript definitions)
npm run build
# Verify build output
ls -la dist/🤝 Contributing
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Development Setup
- Clone the repository
- Install dependencies:
npm install - Run tests:
npm test - Start LocalStack:
npm run dev:setup - Run integration tests:
npm run test:localstack
📄 License
MIT License - see LICENSE file for details.
🔗 Links
- AWS Cognito Documentation
- JWT.io - JWT token debugger
- Express.js - Web framework
- TypeScript - Language documentation
📞 Support
- 🐛 Report Issues
- 💬 Discussions
- 📧 Email: [email protected]
Made with ❤️ by the C3Labs Team
