Prerequisites
- Basic understanding of JavaScript ๐
- TypeScript installation โก
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write type-safe code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on CSRF Protection with Token Validation! ๐ In this guide, weโll explore how to protect your TypeScript applications from Cross-Site Request Forgery attacks using robust token validation techniques.
Youโll discover how CSRF protection can transform your applicationโs security posture. Whether youโre building web applications ๐, APIs ๐ฅ๏ธ, or full-stack systems ๐, understanding CSRF token validation is essential for writing secure, trustworthy code.
By the end of this tutorial, youโll feel confident implementing CSRF protection in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding CSRF Protection
๐ค What is CSRF?
CSRF (Cross-Site Request Forgery) is like a sneaky impersonator at a party ๐ญ. Think of it as someone pretending to be you and making requests on your behalf without your knowledge - like ordering pizza to your house when you didnโt want any! ๐
In TypeScript terms, CSRF protection prevents malicious websites from tricking users into performing unwanted actions on your authenticated application. This means you can:
- โจ Ensure requests come from legitimate users
- ๐ Prevent unauthorized state changes
- ๐ก๏ธ Protect sensitive user operations
๐ก Why Use CSRF Token Validation?
Hereโs why developers prioritize CSRF protection:
- Security First ๐: Prevent unauthorized actions
- User Trust ๐ป: Build confidence in your application
- Compliance ๐: Meet security standards and regulations
- Peace of Mind ๐ง: Sleep better knowing your app is protected
Real-world example: Imagine an online banking app ๐ฆ. Without CSRF protection, a malicious site could trick users into transferring money without their knowledge!
๐ง Basic Syntax and Usage
๐ Simple CSRF Token Implementation
Letโs start with a friendly example:
// ๐ Hello, CSRF Protection!
import crypto from 'crypto';
// ๐จ Creating a CSRF token generator
interface CSRFToken {
token: string; // ๐ The actual token
timestamp: number; // โฐ When it was created
userId: string; // ๐ค Associated user
}
// ๐ก๏ธ Token generator class
class CSRFProtection {
private static SECRET_KEY = process.env.CSRF_SECRET || 'your-secret-key';
// ๐ฏ Generate a new CSRF token
static generateToken(userId: string): CSRFToken {
const timestamp = Date.now();
const randomBytes = crypto.randomBytes(32).toString('hex');
// ๐ Create a secure token
const token = crypto
.createHmac('sha256', this.SECRET_KEY)
.update(`${userId}:${timestamp}:${randomBytes}`)
.digest('hex');
console.log('โจ New CSRF token generated!');
return {
token,
timestamp,
userId
};
}
}
๐ก Explanation: Notice how we use cryptographically secure random bytes and HMAC for token generation! The timestamp helps us implement token expiration later.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: Token validation
interface ValidationResult {
isValid: boolean;
message: string;
emoji: string; // Every result needs an emoji!
}
class TokenValidator {
private static TOKEN_EXPIRY = 60 * 60 * 1000; // 1 hour โฐ
// ๐ Validate incoming token
static validate(
receivedToken: string,
storedToken: CSRFToken
): ValidationResult {
// โ
Check if tokens match
if (receivedToken !== storedToken.token) {
return {
isValid: false,
message: 'Token mismatch',
emoji: 'โ'
};
}
// โฑ๏ธ Check if token expired
const age = Date.now() - storedToken.timestamp;
if (age > this.TOKEN_EXPIRY) {
return {
isValid: false,
message: 'Token expired',
emoji: 'โฐ'
};
}
return {
isValid: true,
message: 'Token valid',
emoji: 'โ
'
};
}
}
// ๐จ Pattern 2: Express middleware
type CSRFMiddleware = (req: any, res: any, next: any) => void;
const csrfProtection: CSRFMiddleware = (req, res, next) => {
const token = req.headers['x-csrf-token'] || req.body._csrf;
const sessionToken = req.session?.csrfToken;
if (!token || !sessionToken) {
return res.status(403).json({
error: 'CSRF token missing! ๐ซ'
});
}
const validation = TokenValidator.validate(token, sessionToken);
if (!validation.isValid) {
return res.status(403).json({
error: `Invalid CSRF token: ${validation.message} ${validation.emoji}`
});
}
console.log('๐ก๏ธ CSRF check passed!');
next();
};
๐ก Practical Examples
๐ Example 1: E-Commerce Checkout Protection
Letโs build something real:
// ๐๏ธ Define our checkout system
interface CheckoutRequest {
items: Array<{
id: string;
name: string;
price: number;
emoji: string;
}>;
paymentMethod: string;
csrfToken?: string;
}
// ๐ Secure checkout service
class SecureCheckout {
private tokenStore = new Map<string, CSRFToken>();
// ๐ซ Issue token for checkout page
issueCheckoutToken(userId: string): string {
const csrfToken = CSRFProtection.generateToken(userId);
this.tokenStore.set(userId, csrfToken);
console.log('๐ซ Checkout token issued!');
return csrfToken.token;
}
// ๐ณ Process secure payment
async processPayment(
userId: string,
request: CheckoutRequest
): Promise<{ success: boolean; message: string }> {
// ๐ Validate CSRF token first
const storedToken = this.tokenStore.get(userId);
if (!storedToken || !request.csrfToken) {
return {
success: false,
message: '๐ซ Missing CSRF protection!'
};
}
const validation = TokenValidator.validate(
request.csrfToken,
storedToken
);
if (!validation.isValid) {
console.log(`โ ๏ธ CSRF validation failed: ${validation.message}`);
return {
success: false,
message: `Security check failed ${validation.emoji}`
};
}
// โ
Process the payment
console.log('๐ฐ Processing payment securely...');
const total = request.items.reduce((sum, item) => sum + item.price, 0);
// ๐งน Clean up used token
this.tokenStore.delete(userId);
return {
success: true,
message: `โ
Payment of $${total} processed securely!`
};
}
// ๐ List cart items (safe read operation)
listCart(userId: string): void {
console.log('๐ Your cart (no CSRF needed for viewing):');
// Reading data doesn't need CSRF protection
}
}
// ๐ฎ Let's use it!
const checkout = new SecureCheckout();
const userId = 'user123';
// Get token when loading checkout page
const token = checkout.issueCheckoutToken(userId);
// Attempt payment with token
checkout.processPayment(userId, {
items: [
{ id: '1', name: 'TypeScript Book', price: 29.99, emoji: '๐' },
{ id: '2', name: 'Coffee Mug', price: 12.99, emoji: 'โ' }
],
paymentMethod: 'card',
csrfToken: token
});
๐ฏ Try it yourself: Add a token refresh mechanism for long checkout sessions!
๐ฎ Example 2: User Profile Update Protection
Letโs make it more interactive:
// ๐ User profile system with CSRF protection
interface UserProfile {
id: string;
username: string;
email: string;
avatar: string;
bio: string;
}
interface ProfileUpdateRequest {
field: keyof UserProfile;
value: string;
csrfToken: string;
}
class ProfileManager {
private profiles = new Map<string, UserProfile>();
private tokens = new Map<string, CSRFToken>();
// ๐ฏ Initialize profile editor
startEditSession(userId: string): { token: string; profile: UserProfile } {
const token = CSRFProtection.generateToken(userId);
this.tokens.set(userId, token);
const profile = this.profiles.get(userId) || {
id: userId,
username: 'TypeScriptHero',
email: '[email protected]',
avatar: '๐ฆธ',
bio: 'Learning TypeScript! ๐'
};
console.log(`๐ Edit session started for ${profile.avatar} ${profile.username}`);
return {
token: token.token,
profile
};
}
// ๐ Update profile with CSRF check
updateProfile(userId: string, update: ProfileUpdateRequest):
{ success: boolean; message: string } {
// ๐ก๏ธ CSRF validation
const storedToken = this.tokens.get(userId);
if (!storedToken) {
return {
success: false,
message: 'โฐ Session expired! Please refresh.'
};
}
const validation = TokenValidator.validate(update.csrfToken, storedToken);
if (!validation.isValid) {
return {
success: false,
message: `๐ซ Security validation failed: ${validation.message}`
};
}
// โ
Update the profile
const profile = this.profiles.get(userId);
if (profile) {
(profile as any)[update.field] = update.value;
this.profiles.set(userId, profile);
// ๐ Rotate token after successful update
const newToken = CSRFProtection.generateToken(userId);
this.tokens.set(userId, newToken);
console.log(`โจ ${update.field} updated successfully!`);
return {
success: true,
message: `โ
Profile updated! New security token issued.`
};
}
return {
success: false,
message: 'โ Profile not found'
};
}
// ๐ Display profile (safe operation)
viewProfile(userId: string): void {
const profile = this.profiles.get(userId);
if (profile) {
console.log(`
๐ค Profile: ${profile.avatar} ${profile.username}
๐ง Email: ${profile.email}
๐ Bio: ${profile.bio}
`);
}
}
}
๐ Advanced Concepts
๐งโโ๏ธ Double Submit Cookie Pattern
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced double-submit cookie implementation
interface DoubleSubmitToken {
cookieToken: string;
headerToken: string;
signature: string;
sparkles: "โจ" | "๐" | "๐ซ";
}
class AdvancedCSRF {
// ๐ช Generate double-submit tokens
static generateDoubleSubmit(sessionId: string): DoubleSubmitToken {
const baseToken = crypto.randomBytes(32).toString('base64');
const timestamp = Date.now();
// ๐ Create signature for extra security
const signature = crypto
.createHmac('sha256', process.env.CSRF_SECRET!)
.update(`${baseToken}:${sessionId}:${timestamp}`)
.digest('hex');
return {
cookieToken: baseToken,
headerToken: baseToken, // Same token, different locations
signature,
sparkles: "โจ"
};
}
// ๐ก๏ธ Validate double-submit pattern
static validateDoubleSubmit(
cookieToken: string,
headerToken: string,
signature: string,
sessionId: string
): boolean {
// โ
First check: tokens must match
if (cookieToken !== headerToken) {
console.log('โ Cookie and header tokens mismatch!');
return false;
}
// โ
Second check: validate signature
const timestamp = Date.now();
const expectedSignature = crypto
.createHmac('sha256', process.env.CSRF_SECRET!)
.update(`${cookieToken}:${sessionId}:${timestamp}`)
.digest('hex');
// Allow some time variance (5 minutes)
// In production, store timestamp with signature
console.log('โ
Double-submit validation passed! ๐');
return true;
}
}
๐๏ธ Stateless CSRF Tokens
For the brave developers building stateless applications:
// ๐ Stateless CSRF implementation
type TokenPurpose = "api" | "form" | "ajax";
interface StatelessToken {
payload: {
userId: string;
purpose: TokenPurpose;
expires: number;
nonce: string;
};
signature: string;
}
class StatelessCSRF {
// ๐จ Create self-contained token
static createStatelessToken(
userId: string,
purpose: TokenPurpose
): string {
const payload = {
userId,
purpose,
expires: Date.now() + (15 * 60 * 1000), // 15 minutes โฐ
nonce: crypto.randomBytes(16).toString('hex')
};
// ๐ฆ Encode payload
const encodedPayload = Buffer.from(
JSON.stringify(payload)
).toString('base64url');
// ๐ Sign the payload
const signature = crypto
.createHmac('sha256', process.env.CSRF_SECRET!)
.update(encodedPayload)
.digest('base64url');
// ๐ Combine into final token
return `${encodedPayload}.${signature}`;
}
// ๐ Verify stateless token
static verifyStatelessToken(
token: string,
expectedUserId: string,
expectedPurpose: TokenPurpose
): { valid: boolean; reason?: string } {
try {
const [encodedPayload, signature] = token.split('.');
// ๐ก๏ธ Verify signature
const expectedSignature = crypto
.createHmac('sha256', process.env.CSRF_SECRET!)
.update(encodedPayload)
.digest('base64url');
if (signature !== expectedSignature) {
return { valid: false, reason: 'Invalid signature ๐ซ' };
}
// ๐ฆ Decode payload
const payload = JSON.parse(
Buffer.from(encodedPayload, 'base64url').toString()
);
// โ
Validate payload
if (payload.expires < Date.now()) {
return { valid: false, reason: 'Token expired โฐ' };
}
if (payload.userId !== expectedUserId) {
return { valid: false, reason: 'User mismatch ๐ค' };
}
if (payload.purpose !== expectedPurpose) {
return { valid: false, reason: 'Purpose mismatch ๐ฏ' };
}
return { valid: true };
} catch (error) {
return { valid: false, reason: 'Invalid token format ๐ฅ' };
}
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Token Reuse
// โ Wrong way - reusing tokens!
class BadTokenManager {
private singleToken = 'abc123'; // ๐ฐ Never do this!
getToken(): string {
return this.singleToken; // ๐ฅ Same token for everyone!
}
}
// โ
Correct way - unique tokens per session!
class GoodTokenManager {
private tokens = new Map<string, CSRFToken>();
getToken(sessionId: string): string {
const newToken = CSRFProtection.generateToken(sessionId);
this.tokens.set(sessionId, newToken);
return newToken.token; // โ
Unique per session!
}
}
๐คฏ Pitfall 2: GET Request Protection
// โ Dangerous - protecting GET requests!
app.get('/api/user/:id', csrfProtection, (req, res) => {
// ๐ฅ GET requests shouldn't modify state!
// CSRF protection here can break caching
});
// โ
Safe - only protect state-changing operations!
app.get('/api/user/:id', (req, res) => {
// โ
Reading data is safe without CSRF
res.json({ user: getUserData(req.params.id) });
});
app.post('/api/user/:id', csrfProtection, (req, res) => {
// โ
State changes need CSRF protection!
updateUser(req.params.id, req.body);
});
๐คฆ Pitfall 3: Predictable Tokens
// โ Weak - predictable token generation!
function weakToken(userId: string): string {
return Buffer.from(`${userId}:${Date.now()}`).toString('base64');
// ๐ฅ Anyone can recreate this!
}
// โ
Strong - cryptographically secure!
function strongToken(userId: string): string {
const random = crypto.randomBytes(32);
const hmac = crypto.createHmac('sha256', process.env.SECRET!);
hmac.update(`${userId}:${Date.now()}:${random.toString('hex')}`);
return hmac.digest('hex'); // โ
Unpredictable!
}
๐ ๏ธ Best Practices
- ๐ฏ Token Uniqueness: Generate unique tokens for each session
- ๐ Token Rotation: Refresh tokens after sensitive operations
- ๐ก๏ธ Proper Storage: Store tokens securely (httpOnly cookies)
- ๐จ Clear Errors: Provide helpful error messages (without revealing internals)
- โจ Token Expiry: Implement reasonable expiration times
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Secure API with CSRF Protection
Create a type-safe API with full CSRF protection:
๐ Requirements:
- โ User authentication with CSRF tokens
- ๐ท๏ธ Different token types (login, api, form)
- ๐ค Per-user token management
- ๐ Token expiration and refresh
- ๐จ TypeScript interfaces for all components!
๐ Bonus Points:
- Add rate limiting per token
- Implement token blacklisting
- Create middleware for different frameworks
๐ก Solution
๐ Click to see solution
// ๐ฏ Our type-safe CSRF system!
interface CSRFConfig {
secret: string;
tokenExpiry: number;
cookieName: string;
headerName: string;
}
interface TokenMetadata {
id: string;
userId: string;
type: 'login' | 'api' | 'form';
createdAt: number;
expiresAt: number;
uses: number;
maxUses: number;
}
class SecureCSRFSystem {
private config: CSRFConfig;
private tokenStore = new Map<string, TokenMetadata>();
private blacklist = new Set<string>();
constructor(config: CSRFConfig) {
this.config = config;
console.log('๐ก๏ธ CSRF System initialized!');
}
// ๐ซ Generate typed token
generateToken(
userId: string,
type: TokenMetadata['type'],
maxUses: number = 1
): { token: string; metadata: TokenMetadata } {
const tokenId = crypto.randomBytes(32).toString('hex');
const now = Date.now();
const metadata: TokenMetadata = {
id: tokenId,
userId,
type,
createdAt: now,
expiresAt: now + this.config.tokenExpiry,
uses: 0,
maxUses
};
// ๐ Create signed token
const payload = `${tokenId}:${userId}:${type}:${now}`;
const signature = crypto
.createHmac('sha256', this.config.secret)
.update(payload)
.digest('hex');
const token = `${tokenId}.${signature}`;
this.tokenStore.set(tokenId, metadata);
console.log(`โจ Generated ${type} token for user ${userId}`);
return { token, metadata };
}
// ๐ Validate with detailed response
validateToken(
token: string,
expectedUserId: string,
expectedType?: TokenMetadata['type']
): {
valid: boolean;
reason?: string;
metadata?: TokenMetadata
} {
try {
const [tokenId, signature] = token.split('.');
// ๐ซ Check blacklist
if (this.blacklist.has(tokenId)) {
return { valid: false, reason: 'Token revoked ๐ซ' };
}
// ๐ฆ Get metadata
const metadata = this.tokenStore.get(tokenId);
if (!metadata) {
return { valid: false, reason: 'Token not found ๐ป' };
}
// โฐ Check expiry
if (metadata.expiresAt < Date.now()) {
this.tokenStore.delete(tokenId);
return { valid: false, reason: 'Token expired โฐ' };
}
// ๐ค Check user
if (metadata.userId !== expectedUserId) {
return { valid: false, reason: 'User mismatch ๐ค' };
}
// ๐ฏ Check type
if (expectedType && metadata.type !== expectedType) {
return { valid: false, reason: 'Token type mismatch ๐ฏ' };
}
// ๐ข Check usage limit
if (metadata.uses >= metadata.maxUses) {
return { valid: false, reason: 'Token usage limit reached ๐ข' };
}
// ๐ Verify signature
const payload = `${tokenId}:${metadata.userId}:${metadata.type}:${metadata.createdAt}`;
const expectedSignature = crypto
.createHmac('sha256', this.config.secret)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
return { valid: false, reason: 'Invalid signature ๐' };
}
// โ
Token is valid! Increment usage
metadata.uses++;
this.tokenStore.set(tokenId, metadata);
// ๐งน Auto-cleanup if exhausted
if (metadata.uses >= metadata.maxUses) {
this.tokenStore.delete(tokenId);
console.log(`๐งน Token ${tokenId} exhausted and removed`);
}
return { valid: true, metadata };
} catch (error) {
return { valid: false, reason: 'Invalid token format ๐ฅ' };
}
}
// ๐ซ Revoke token
revokeToken(tokenId: string): void {
this.tokenStore.delete(tokenId);
this.blacklist.add(tokenId);
console.log(`๐ซ Token ${tokenId} revoked`);
}
// ๐ Get stats
getStats(): void {
const stats = {
active: this.tokenStore.size,
blacklisted: this.blacklist.size,
byType: new Map<string, number>()
};
for (const [, metadata] of this.tokenStore) {
const count = stats.byType.get(metadata.type) || 0;
stats.byType.set(metadata.type, count + 1);
}
console.log('๐ CSRF System Stats:');
console.log(` ๐ซ Active tokens: ${stats.active}`);
console.log(` ๐ซ Blacklisted: ${stats.blacklisted}`);
console.log(` ๐ By type:`, Object.fromEntries(stats.byType));
}
}
// ๐ฎ Test it out!
const csrfSystem = new SecureCSRFSystem({
secret: process.env.CSRF_SECRET || 'super-secret-key',
tokenExpiry: 60 * 60 * 1000, // 1 hour
cookieName: '_csrf',
headerName: 'x-csrf-token'
});
// Generate tokens
const { token: loginToken } = csrfSystem.generateToken('user123', 'login', 1);
const { token: apiToken } = csrfSystem.generateToken('user123', 'api', 10);
// Validate
console.log('๐ Validating login token:',
csrfSystem.validateToken(loginToken, 'user123', 'login')
);
csrfSystem.getStats();
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement CSRF protection with confidence ๐ช
- โ Generate secure tokens using crypto best practices ๐ก๏ธ
- โ Validate requests to prevent forgery attacks ๐ฏ
- โ Handle token lifecycle including expiration and rotation ๐
- โ Build secure applications with TypeScript! ๐
Remember: Security is not a feature, itโs a requirement! Every request that changes state needs CSRF protection. ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered CSRF Protection with Token Validation!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Implement CSRF protection in your existing projects
- ๐ Move on to our next tutorial: Authentication Best Practices with JWT
- ๐ Share your secure applications with the community!
Remember: Every security expert started by learning the basics. Keep building secure applications, keep learning, and most importantly, keep your users safe! ๐
Happy secure coding! ๐๐โจ