Understanding Serverless Authentication
Authentication in serverless architectures requires a different approach than traditional monolithic applications. Instead of maintaining session state on servers, serverless auth leverages stateless JWT tokens, managed services, and distributed validation.
Traditional vs Serverless Auth
typescript
// Traditional: Session-based
// 1. User logs in
// 2. Server creates session in database
// 3. Server returns session cookie
// 4. Each request validates session against database
// Problem: Doesn't scale well with serverless
// Serverless: Token-based
// 1. User logs in
// 2. Auth service issues JWT token
// 3. Client stores token
// 4. Each request validates token signature
// Benefit: Stateless, scales infinitelyAuthentication Architecture
Core Components
typescript
// Architecture Overview
┌─────────────┐
│ Client │
└──────┬──────┘
│
│ 1. Login Request
▼
┌─────────────────────────┐
│ API Gateway / Cognito │
└──────┬──────────────────┘
│
│ 2. Validate Credentials
▼
┌─────────────────────────┐
│ Lambda Authorizer │ ◄───┐
└──────┬──────────────────┘ │
│ │
│ 3. Issue/Validate JWT │
▼ │
┌─────────────────────────┐ │
│ Lambda Function │─────┘
└─────────────────────────┘AWS Cognito Integration
User Pool Setup
typescript
import {
CognitoIdentityProviderClient,
AdminCreateUserCommand,
AdminInitiateAuthCommand,
AdminRespondToAuthChallengeCommand,
} from '@aws-sdk/client-cognito-identity-provider';
const client = new CognitoIdentityProviderClient({
region: process.env.AWS_REGION,
});
const USER_POOL_ID = process.env.USER_POOL_ID;
const CLIENT_ID = process.env.COGNITO_CLIENT_ID;
// Register new user
export const handler = async (event: any) => {
const { email, password, name } = JSON.parse(event.body);
const command = new AdminCreateUserCommand({
UserPoolId: USER_POOL_ID,
Username: email,
TemporaryPassword: password,
MessageAction: 'SUPPRESS', // Don't send welcome email
UserAttributes: [
{
Name: 'email',
Value: email,
},
{
Name: 'name',
Value: name,
},
{
Name: 'email_verified',
Value: 'true',
},
],
});
try {
const result = await client.send(command);
// Permanent password
await setPassword(email, password);
return {
statusCode: 201,
body: JSON.stringify({
userId: result.User?.Attributes?.find((attr) => attr.Name === 'sub')?.Value,
message: 'User created successfully',
}),
};
} catch (error) {
return {
statusCode: 400,
body: JSON.stringify({ error: error.message }),
};
}
};
async function setPassword(username: string, password: string) {
const authCommand = new AdminInitiateAuthCommand({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
AuthFlow: 'ADMIN_NO_SRP_AUTH',
AuthParameters: {
USERNAME: username,
PASSWORD: password,
},
});
const authResult = await client.send(authCommand);
// Complete password challenge
if (authResult.ChallengeName === 'NEW_PASSWORD_REQUIRED') {
await client.send(new AdminRespondToAuthChallengeCommand({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
ChallengeName: 'NEW_PASSWORD_REQUIRED',
Session: authResult.Session,
ChallengeResponses: {
USERNAME: username,
NEW_PASSWORD: password,
},
}));
}
}Login Handler
typescript
export const loginHandler = async (event: any) => {
const { email, password } = JSON.parse(event.body);
const command = new AdminInitiateAuthCommand({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
AuthFlow: 'ADMIN_NO_SRP_AUTH',
AuthParameters: {
USERNAME: email,
PASSWORD: password,
},
});
try {
const result = await client.send(command);
// Handle MFA challenge if enabled
if (result.ChallengeName === 'SOFTWARE_TOKEN_MFA') {
return {
statusCode: 200,
body: JSON.stringify({
requiresMFA: true,
session: result.Session,
}),
};
}
return {
statusCode: 200,
body: JSON.stringify({
accessToken: result.AuthenticationResult?.AccessToken,
idToken: result.AuthenticationResult?.IdToken,
refreshToken: result.AuthenticationResult?.RefreshToken,
expiresIn: result.AuthenticationResult?.ExpiresIn,
}),
};
} catch (error) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Invalid credentials' }),
};
}
};Lambda Authorizers
Custom Authorizer
typescript
import {
APIGatewayTokenAuthorizerEvent,
APIGatewayAuthorizerResult,
PolicyDocument,
Statement,
} from 'aws-lambda';
import { verify } from 'jsonwebtoken';
import { JwtPayload } from 'jsonwebtoken';
// Cognito JWKS URL
const JWKS_URL = `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`;
interface AuthorizerContext {
userId: string;
email: string;
}
export const authorizerHandler = async (
event: APIGatewayTokenAuthorizerEvent
): Promise<APIGatewayAuthorizerResult> => {
try {
const token = event.authorizationToken?.replace('Bearer ', '');
if (!token) {
throw new Error('No token provided');
}
// Verify JWT token
const decoded = await verifyToken(token);
// Generate IAM policy
const policy = generatePolicy(
decoded.sub,
'Allow',
event.methodArn,
{
userId: decoded.sub,
email: decoded.email,
}
);
return policy;
} catch (error) {
console.error('Authorization failed:', error);
return generatePolicy('user', 'Deny', event.methodArn);
}
};
async function verifyToken(token: string): Promise<JwtPayload> {
// Fetch JWKS
const response = await fetch(JWKS_URL);
const jwks = await response.json();
// Get key ID from token header
const decodedToken = verify(token, getSecretKey(jwks, token), {
issuer: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.USER_POOL_ID}`,
audience: process.env.CLIENT_ID,
});
return decodedToken as JwtPayload;
}
function generatePolicy(
principalId: string,
effect: 'Allow' | 'Deny',
resource: string,
context?: AuthorizerContext
): APIGatewayAuthorizerResult {
const authResponse: APIGatewayAuthorizerResult = {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
},
};
if (context) {
authResponse.context = context;
}
return authResponse;
}Using Authorizer in Serverless Framework
yaml
# serverless.yml
service: auth-service
provider:
name: aws
runtime: nodejs18.x
environment:
USER_POOL_ID: !Ref CognitoUserPool
CLIENT_ID: !Ref CognitoUserPoolClient
functions:
authorizer:
handler: authorizer.handler
name: token-authorizer
getUser:
handler: getUser.handler
events:
- http:
path: /users/{id}
method: get
authorizer:
name: authorizer
type: TOKEN
identitySource: method.request.header.Authorization
resources:
Resources:
CognitoUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: my-auth-pool
UsernameAttributes:
- email
AutoVerifiedAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireUppercase: true
RequireNumbers: true
CognitoUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref CognitoUserPool
ExplicitAuthFlows:
- ADMIN_NO_SRP_AUTH
GenerateSecret: falseJWT Token Management
Token Generation and Refresh
typescript
import { sign } from 'jsonwebtoken';
interface TokenPayload {
userId: string;
email: string;
roles: string[];
}
export const tokenHandler = async (event: any) => {
const { refreshToken } = JSON.parse(event.body);
try {
// Verify refresh token with Cognito
const command = new AdminInitiateAuthCommand({
UserPoolId: USER_POOL_ID,
ClientId: CLIENT_ID,
AuthFlow: 'REFRESH_TOKEN_AUTH',
AuthParameters: {
REFRESH_TOKEN: refreshToken,
},
});
const result = await client.send(command);
return {
statusCode: 200,
body: JSON.stringify({
accessToken: result.AuthenticationResult?.AccessToken,
idToken: result.AuthenticationResult?.IdToken,
expiresIn: result.AuthenticationResult?.ExpiresIn,
}),
};
} catch (error) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Invalid refresh token' }),
};
}
};
// Custom JWT generation (for microservices)
function generateAccessToken(payload: TokenPayload): string {
return sign(payload, process.env.JWT_SECRET, {
expiresIn: '15m',
issuer: 'my-auth-service',
audience: 'my-api',
});
}
function generateRefreshToken(payload: TokenPayload): string {
return sign(payload, process.env.JWT_SECRET, {
expiresIn: '7d',
issuer: 'my-auth-service',
});
}Token Validation Middleware
typescript
import { Request, Response, NextFunction } from 'express';
import { verify } from 'jsonwebtoken';
interface AuthenticatedRequest extends Request {
user?: TokenPayload;
}
export const authenticate = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
const decoded = verify(token, process.env.JWT_SECRET!) as TokenPayload;
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};
export const authorize = (...roles: string[]) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
const hasRole = roles.some((role) => req.user!.roles.includes(role));
if (!hasRole) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};Role-Based Access Control
Cognito Groups
typescript
import {
AdminAddUserToGroupCommand,
CreateGroupCommand,
} from '@aws-sdk/client-cognito-identity-provider';
async function setupUserGroups() {
// Create groups
const groups = ['Admin', 'User', 'Moderator'];
for (const group of groups) {
await client.send(new CreateGroupCommand({
UserPoolId: USER_POOL_ID,
GroupName: group,
}));
}
}
async function addUserToGroup(userId: string, group: string) {
await client.send(new AdminAddUserToGroupCommand({
UserPoolId: USER_POOL_ID,
Username: userId,
GroupName: group,
}));
}
// In authorizer, check group membership
async function getUserGroups(userId: string): Promise<string[]> {
const command = new AdminListGroupsForUserCommand({
UserPoolId: USER_POOL_ID,
Username: userId,
});
const result = await client.send(command);
return result.Groups?.map((group) => group.GroupName || '') || [];
}Fine-Grained Permissions
typescript
interface Permission {
resource: string;
actions: string[];
}
const rolePermissions: Record<string, Permission[]> = {
Admin: [
{ resource: '*', actions: ['*'] },
],
User: [
{ resource: 'posts', actions: ['read', 'create'] },
{ resource: 'comments', actions: ['read', 'create'] },
],
Moderator: [
{ resource: 'posts', actions: ['read', 'update', 'delete'] },
{ resource: 'comments', actions: ['read', 'delete'] },
],
};
function checkPermission(
role: string,
resource: string,
action: string
): boolean {
const permissions = rolePermissions[role] || [];
return permissions.some((permission) => {
const matchesResource = permission.resource === '*' ||
permission.resource === resource;
const matchesAction = permission.actions.includes('*') ||
permission.actions.includes(action);
return matchesResource && matchesAction;
});
}Multi-Factor Authentication (MFA)
TOTP Setup
typescript
import {
AssociateSoftwareTokenCommand,
VerifySoftwareTokenCommand,
SetUserMFAPreferenceCommand,
} from '@aws-sdk/client-cognito-identity-provider';
export const setupMFA = async (event: any) => {
const { userId } = event.requestContext.authorizer;
// Associate software token
const associateCommand = new AssociateSoftwareTokenCommand({
UserPoolId: USER_POOL_ID,
Username: userId,
});
const result = await client.send(associateCommand);
return {
statusCode: 200,
body: JSON.stringify({
secretCode: result.SecretCode,
}),
};
};
export const verifyMFA = async (event: any) => {
const { userId } = event.requestContext.authorizer;
const { code, session } = JSON.parse(event.body);
const verifyCommand = new VerifySoftwareTokenCommand({
UserPoolId: USER_POOL_ID,
Username: userId,
Session: session,
UserCode: code,
});
const result = await client.send(verifyCommand);
// Enable MFA
await client.send(new SetUserMFAPreferenceCommand({
UserPoolId: USER_POOL_ID,
Username: userId,
SoftwareTokenMfaSettings: {
Enabled: true,
PreferredMfa: true,
},
}));
return {
statusCode: 200,
body: JSON.stringify({
message: 'MFA enabled successfully',
}),
};
};Social Login Integration
Google OAuth
typescript
import axios from 'axios';
export const googleAuthHandler = async (event: any) => {
const { code } = JSON.parse(event.body);
// Exchange code for tokens
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
redirect_uri: process.env.GOOGLE_REDIRECT_URI,
grant_type: 'authorization_code',
}
);
// Get user info
const userInfoResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${tokenResponse.data.access_token}`,
},
}
);
const userInfo = userInfoResponse.data;
// Find or create user in Cognito
const userId = await findOrCreateUser(userInfo);
// Return tokens
return {
statusCode: 200,
body: JSON.stringify({
userId,
email: userInfo.email,
name: userInfo.name,
}),
};
};
async function findOrCreateUser(userInfo: any): Promise<string> {
// Check if user exists
// If not, create with Google info
// Return user ID
}Security Best Practices
Token Storage
typescript
// Frontend: Store tokens securely
// ✓ Use httpOnly cookies
// ✓ Use secure flag (HTTPS only)
// ✓ Use sameSite flag
// ❌ Don't store in localStorage (XSS vulnerable)
// ✓ Use httpOnly cookies
// Set cookie on login
response.setHeader('Set-Cookie', [
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=900`, // 15 min
`refreshToken=${refreshToken}; HttpOnly; Secure; SameSite=Strict; Max-Age=604800`, // 7 days
]);Rate Limiting
typescript
// Prevent brute force attacks
import RateLimit from 'express-rate-limit';
const loginLimiter = RateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many login attempts',
});
app.use('/auth/login', loginLimiter);Monitoring and Logging
CloudWatch Metrics
typescript
import { CloudWatch } from '@aws-sdk/client-cloudwatch';
const cloudwatch = new CloudWatch({ region: process.env.AWS_REGION });
async function trackLogin(success: boolean, userId: string) {
await cloudwatch.putMetricData({
Namespace: 'Authentication',
MetricData: [{
MetricName: 'LoginAttempts',
Dimensions: [{
Name: 'Success',
Value: success.toString(),
}],
Value: 1,
Unit: 'Count',
}],
});
}Testing Authentication
Unit Tests
typescript
import { verify } from 'jsonwebtoken';
describe('Authentication', () => {
it('should validate JWT token', () => {
const token = generateToken({ userId: '123', email: 'test@example.com' });
const decoded = verify(token, process.env.JWT_SECRET);
expect(decoded.userId).toBe('123');
expect(decoded.email).toBe('test@example.com');
});
it('should reject invalid token', () => {
const invalidToken = 'invalid.token.here';
expect(() => {
verify(invalidToken, process.env.JWT_SECRET);
}).toThrow();
});
});Conclusion
Serverless authentication leverages managed services and stateless tokens to create secure, scalable auth systems. By using AWS Cognito, Lambda authorizers, and JWT tokens, you can build enterprise-grade authentication without managing servers.
Key benefits:
- Scalable without managing infrastructure
- Built-in security best practices
- Support for multiple authentication methods
- Fine-grained access control
- Easy integration with other AWS services
Build secure serverless authentication systems today!