Skip to content

CodaCrew-Code-Labs/GateKeeper

Repository files navigation

@gateway/cognito-auth

npm version License: MIT Node.js Version

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-auth

Peer 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-secret
import { 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 ID
  • config.clientId (string) - AWS Cognito App Client ID
  • config.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('user@example.com', '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 format
  • AuthenticationError - 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('user@example.com');
  console.log('Password reset link sent to email');
} catch (error) {
  console.error('Password reset request failed:', error.message);
}

Throws:

  • ValidationError - Invalid email format
  • AuthenticationError - 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('user@example.com', '123456', 'NewSecurePassword123!');
  console.log('Password reset successful');
} catch (error) {
  console.error('Password reset failed:', error.message);
}

Throws:

  • ValidationError - Invalid input format
  • AuthenticationError - 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('user@example.com', '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 format
  • AuthenticationError - 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 format
  • AuthenticationError - 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 verify
  • skipVerification (boolean, optional) - Skip verification for development

Express Middleware

The middleware automatically:

  • Extracts Bearer tokens from Authorization headers
  • 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:watch

Property-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:stop

Building 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

  1. Clone the repository
  2. Install dependencies: npm install
  3. Run tests: npm test
  4. Start LocalStack: npm run dev:setup
  5. Run integration tests: npm run test:localstack

πŸ“„ License

MIT License - see LICENSE file for details.

πŸ”— Links

πŸ“ž Support


Made with ❀️ by the C3Labs Team

About

A NodeJS based Cognito authentication manager

Resources

License

Contributing

Security policy

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •