Serverless Authentication Setup
Authentication in serverless applications requires a different approach than session-based auth on traditional servers. There is no persistent server to store sessions, so you need stateless token-based authentication with JWTs, proper token validation at the API gateway layer, and secure credential storage. We implement serverless authentication using Amazon Cognito, Auth0, or custom JWT flows with Lambda authorizers, refresh token rotation, and multi-tenant identity isolation.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
Amazon Cognito Setup
We configure Cognito User Pools with secure defaults, custom attributes for multi-tenancy, and pre/post authentication Lambda triggers for business logic.
resource "aws_cognito_user_pool" "main" {
name = "${var.project}-${var.env}"
password_policy {
minimum_length = 12
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
}
account_recovery_setting {
recovery_mechanism {
name = "verified_email"
priority = 1
}
}
schema {
name = "tenant_id"
attribute_data_type = "String"
mutable = false
required = false
string_attribute_constraints {
min_length = 1
max_length = 64
}
}
lambda_config {
pre_sign_up = aws_lambda_function.pre_signup.arn
post_confirmation = aws_lambda_function.post_confirmation.arn
pre_token_generation = aws_lambda_function.pre_token.arn
}
user_pool_add_ons {
advanced_security_mode = "ENFORCED" # Adaptive auth
}
}
resource "aws_cognito_user_pool_client" "web" {
name = "web-client"
user_pool_id = aws_cognito_user_pool.main.id
explicit_auth_flows = [
"ALLOW_USER_SRP_AUTH",
"ALLOW_REFRESH_TOKEN_AUTH",
]
token_validity_units {
access_token = "minutes"
id_token = "minutes"
refresh_token = "days"
}
access_token_validity = 15 # 15 minutes
id_token_validity = 15
refresh_token_validity = 30 # 30 days
prevent_user_existence_errors = "ENABLED"
}We configure short-lived access tokens (15 minutes) with longer refresh tokens (30 days) and rotation on use. The prevent_user_existence_errors setting prevents username enumeration attacks. Advanced security mode enables adaptive authentication with risk-based MFA.
Lambda Authorizer Implementation
For APIs that need custom authorization logic beyond JWT validation, we implement Lambda authorizers with caching and multi-source token support.
import { APIGatewayRequestAuthorizerEventV2 } from 'aws-lambda';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_POOL_ID!,
tokenUse: 'access',
clientId: process.env.COGNITO_CLIENT_ID!,
});
export const handler = async (event: APIGatewayRequestAuthorizerEventV2) => {
// Support both header and cookie-based tokens
const token = extractToken(event);
if (!token) return { isAuthorized: false };
try {
const payload = await verifier.verify(token);
return {
isAuthorized: true,
context: {
userId: payload.sub,
tenantId: payload['custom:tenant_id'] || 'default',
email: payload.email,
groups: (payload['cognito:groups'] || []).join(','),
},
};
} catch (err) {
console.warn('Token verification failed:', (err as Error).message);
return { isAuthorized: false };
}
};
function extractToken(event: APIGatewayRequestAuthorizerEventV2): string | null {
// Authorization header
const authHeader = event.headers?.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.slice(7);
}
// Cookie fallback for web apps
const cookies = event.cookies || [];
const tokenCookie = cookies.find(c => c.startsWith('access_token='));
return tokenCookie ? tokenCookie.split('=')[1] : null;
}The aws-jwt-verify library caches JWKS keys locally, so subsequent verifications do not make HTTP calls. API Gateway caches the authorizer response for the TTL you configure (default: 300 seconds), reducing Lambda invocations for authenticated users to near zero.
Custom JWT with Refresh Token Rotation
For applications that need full control over the authentication flow, we implement custom JWT issuance with refresh token rotation and revocation.
import { SignJWT, jwtVerify, generateKeyPair } from 'jose';
import { randomUUID } from 'crypto';
const ISSUER = 'https://api.yourdomain.com';
const ACCESS_TTL = '15m';
const REFRESH_TTL = '30d';
// Key pair stored in Secrets Manager, loaded at cold start
let keyPair: CryptoKeyPair;
async function getKeyPair() {
if (!keyPair) {
const secret = await getSecret('jwt-keys');
keyPair = JSON.parse(secret);
}
return keyPair;
}
export async function issueTokens(userId: string, tenantId: string) {
const { privateKey } = await getKeyPair();
const jti = randomUUID();
const accessToken = await new SignJWT({ sub: userId, tid: tenantId })
.setProtectedHeader({ alg: 'ES256' })
.setIssuedAt()
.setIssuer(ISSUER)
.setExpirationTime(ACCESS_TTL)
.sign(privateKey);
const refreshToken = await new SignJWT({ sub: userId, jti })
.setProtectedHeader({ alg: 'ES256' })
.setIssuedAt()
.setIssuer(ISSUER)
.setExpirationTime(REFRESH_TTL)
.sign(privateKey);
// Store refresh token family for rotation detection
await docClient.send(new PutCommand({
TableName: 'refresh_tokens',
Item: {
PK: `USER#${userId}`,
SK: `REFRESH#${jti}`,
family: jti,
used: false,
expiresAt: Math.floor(Date.now() / 1000) + 30 * 86400,
},
}));
return { accessToken, refreshToken };
}We use ES256 (ECDSA) signing for smaller tokens and faster verification than RS256. Refresh token rotation means every refresh request issues a new refresh token and invalidates the old one. If a stolen refresh token is replayed, the entire token family is revoked — protecting against token theft.
Multi-Tenant Identity Isolation
For SaaS applications, we implement tenant-scoped authentication that prevents cross-tenant access at the identity layer.
// Pre-token generation Lambda trigger
// Adds tenant claims to Cognito tokens
export const handler = async (event: PreTokenGenerationTriggerEvent) => {
const tenantId = event.request.userAttributes['custom:tenant_id'];
if (!tenantId) {
throw new Error('User has no tenant assignment');
}
// Fetch tenant-specific permissions
const permissions = await getTenantPermissions(tenantId, event.userName);
event.response = {
claimsOverrideDetails: {
claimsToAddOrOverride: {
tenant_id: tenantId,
permissions: JSON.stringify(permissions),
plan: permissions.plan,
},
},
};
return event;
};
// Middleware — enforce tenant scope on every request
export function requireTenant(allowedPlans?: string[]) {
return (event: APIGatewayProxyEventV2) => {
const ctx = event.requestContext.authorizer?.lambda;
if (!ctx?.tenantId) {
return { statusCode: 403, body: 'No tenant context' };
}
if (allowedPlans && !allowedPlans.includes(ctx.plan)) {
return { statusCode: 403, body: 'Plan does not include this feature' };
}
return null; // Proceed
};
}Tenant ID is immutable in the user profile and embedded in every JWT. The authorization middleware validates tenant context on every request. Plan-based feature gating is enforced at the API layer, not the frontend, so it cannot be bypassed. We test cross-tenant isolation with integration tests that attempt to access resources across tenant boundaries.
Why Anubiz Engineering
Ready to get started?
Skip the research. Tell us what you need, and we'll scope it, implement it, and hand it back — fully documented and production-ready.