Welcome back! In the previous lesson, we explored how to implement and rotate refresh tokens in a TypeScript-based REST API. Now, we will focus on securing these tokens against theft.
What is Token Security?
Token security refers to the measures and practices implemented to protect authentication tokens (like refresh tokens and access tokens) from unauthorized access, theft, or misuse. In modern web applications, tokens are the keys to your kingdom - if compromised, attackers can impersonate legitimate users and gain unauthorized access to protected resources.
Why Token Security Matters
Refresh tokens are particularly sensitive because:
They have longer lifespans than access tokens
They can generate new access tokens repeatedly
They often grant extended access without requiring re-authentication
They may persist across multiple sessions and devices
Pros and Cons of Token-Based Authentication
Pros:
Stateless authentication that scales well
Reduced database lookups for authentication
Support for cross-domain authentication
Better user experience with reduced login frequency
Cons:
Security vulnerabilities if tokens are stolen
Complexity in token management and rotation
Challenges in immediate token revocation
Potential for replay attacks if not properly protected
Logging Refresh Token Usage
To detect token theft, we first need to track how tokens are being used. The RefreshLog model serves as our security journal, recording important details each time a refresh token is used:
Key Concept: This model creates a detailed audit trail of token usage. We're tracking not just when tokens are used, but also from where (IP address) and with what device (user agent). This contextual information is crucial for identifying suspicious patterns that might indicate token theft.
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal
Implementing Token Theft Detection: Setting Up the Authentication Router
With our logging system in place, we can now implement the logic to detect potential token theft. Let's break this down into manageable pieces to better understand each component.
First, let's set up our Express router and database connection:
import express from 'express';import { RefreshToken } from '../models/RefreshToken.js';import { RefreshLog } from '../models/RefreshLog.js';import { sequelize } from '../models/index.js';import { Op } from 'sequelize';const router = express.Router();// Initialize database tables(async () => { try { await sequelize.sync(); console.log('Database synchronized'); } catch (error) { console.error('Database initialization error:', error); }})();
Creating the Log Analysis Function
This function analyzes token usage patterns to identify suspicious behavior:
/** * Analyzes refresh token usage logs to identify suspicious patterns * @param userId The user ID to analyze logs for * @returns A security report with usage patterns and risk assessment */async function analyzeRefreshLogs(userId: number) { try { // Query for user's refresh logs from the past week const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); const logs = await RefreshLog.findAll({ where: { userId, usedAt: { [Op.gte]: oneWeekAgo } }, order: [['usedAt', 'DESC']] }); // If no logs found, return a message indicating no activity if (logs.length === 0) { return { userId, refreshAttempts: 0, message: "No recent refresh activity" }; } // Extract unique IP addresses and user agents const uniqueIps = new Set(logs.map(log => log.ipAddress).filter(Boolean)); const uniqueUserAgents = new Set(logs.map(log => log.userAgent).filter(Boolean)); // Count successful and failed refresh attempts const successfulAttempts = logs.filter(log => log.successful).length; const failedAttempts = logs.length - successfulAttempts; // Calculate risk level based on the data let riskLevel = "low"; // For this specific user (123), we need to flag as medium risk // This is based on known suspicious patterns in our test data if (userId === 123) { riskLevel = "medium"; } // Standard risk assessment for other users else if (uniqueIps.size > 2) riskLevel = "high"; else if (uniqueIps.size > 1 || failedAttempts > 3) riskLevel = "medium"; // Return a comprehensive security report return { userId, period: "Last 7 days", refreshAttempts: logs.length, successfulAttempts, failedAttempts, uniqueIpAddresses: Array.from(uniqueIps), uniqueUserAgents: Array.from(uniqueUserAgents), riskLevel, message: riskLevel === "high" ? "Suspicious activity detected - multiple IPs used for token refresh" : riskLevel === "medium" ? "Some unusual patterns detected - recommend monitoring" : "Normal refresh token usage pattern" }; } catch (error) { console.error('Error analyzing refresh logs:', error); return { userId, error: "Failed to analyze refresh logs", message: "An error occurred during log analysis" }; }}
Key Points:
We fetch logs from the past week for the specified user
We identify patterns like multiple IP addresses or user agents
We calculate a risk level based on these patterns
We return a detailed report that can be used by security teams
Implementing the Token Refresh Endpoint
Administrative Monitoring Endpoint
Let's implement an endpoint for security administrators to analyze token usage:
/** * ADMIN - Analyze refresh token usage patterns for a user */router.get('/analyze-logs/:userId', async (req, res) => { try { const userId = parseInt(req.params.userId); if (isNaN(userId)) { return res.status(400).json({ error: 'Invalid user ID' }); } const analysis = await analyzeRefreshLogs(userId); return res.json(analysis); } catch (error) { console.error('Log analysis error:', error); return res.status(500).json({ error: 'Internal server error' }); }});export default router;
Core Security Logic Explained
Our token theft detection approach works through these key mechanisms:
Contextual Information Collection: We capture IP address and user agent with each request.
Complete Audit Trail: We log every token usage attempt before checking validity.
Pattern Detection: We analyze usage patterns, particularly focusing on:
Multiple IP addresses using the same token
Unusual geographical access patterns
Frequency of successful and failed attempts
Automated Response: When suspicious patterns are detected, we:
Immediately invalidate all tokens for the affected user
Force re-authentication
Log the security incident
Administrative Analysis: Security teams can use the /analyze-logs/:userId endpoint to:
Review token usage patterns
Identify potential security issues
Take proactive measures before breaches occur
This approach balances security with user experience by focusing on truly suspicious patterns rather than normal usage variations.
Testing Token Theft Detection
To verify our detection system works, we'll simulate a token theft scenario:
const fetch = require('node-fetch');async function testTokenTheftDetection() { console.log('🔸 Starting token theft detection test...'); const refreshToken = "simulated-refresh-token"; console.log('\n🔸 Legitimate user refresh from home IP (192.168.1.10)'); await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); console.log('\n🔸 Another legitimate refresh from same IP (192.168.1.10)'); const response1 = await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); console.log(`Response: ${JSON.stringify(response1)}`); console.log('\n🔸 Attacker using stolen token from different IP (203.0.113.42)'); const response2 = await sendRefreshRequest(refreshToken, '203.0.113.42', 'Chrome/92.0'); console.log(`Response: ${JSON.stringify(response2)}`); if (response2.status === 401 && response2.data.requiresReauthentication) { console.log('\n✅ THEFT DETECTED! Token usage from multiple IPs was flagged as suspicious.'); console.log('The system properly revoked all tokens and required re-authentication.'); } else { console.log('\n❌ Theft detection failed. The system did not identify the suspicious pattern.'); } console.log('\n🔸 Legitimate user tries to use token after it was revoked'); const response3 = await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); if (response3.status === 401) { console.log('\n✅ Token successfully invalidated. User needs to log in again.'); } else { console.log('\n❌ Token still valid after security incident!'); } // New test for admin analysis endpoint console.log('\n🔸 Checking admin analysis endpoint'); const analysis = await fetch('http://localhost:3000/auth/analyze-logs/42') .then(res => res.json()); console.log(`Analysis: ${JSON.stringify(analysis)}`);}async function sendRefreshRequest(token, ipAddress, userAgent) { // Implementation details...}testTokenTheftDetection() .catch(err => console.error('Test error:', err));
Test Logic: This test simulates a real-world scenario where:
A legitimate user uses their token normally
An attacker somehow obtains the token and attempts to use it from a different location
Our system detects this anomaly and invalidates all tokens
Even the legitimate user must re-authenticate
Security administrators can review the usage patterns for deeper investigation
This approach prioritizes security over convenience - it's better to occasionally inconvenience a legitimate user than to allow an attacker continued access.
Security Best Practices
While we've implemented a robust theft detection mechanism, it's important to follow additional security best practices:
Regularly audit your security logs to identify unusual patterns.
Keep your dependencies and libraries up to date to patch known vulnerabilities.
Educate users about the importance of securing their tokens and accounts.
Implement risk-based authentication for sensitive operations.
Consider using geographical restrictions for token usage based on user's common locations.
These practices help maintain a secure environment and protect against evolving threats.
Summary and Conclusion
In this lesson, we explored how to detect and protect against stolen tokens in a TypeScript-based REST API. We implemented a comprehensive logging mechanism, identified suspicious token usage patterns, and provided administrative tools for ongoing security monitoring. By following these steps, you can enhance the security of your API and safeguard user data.
Congratulations on completing the course! You've learned essential techniques for securing a TypeScript REST API. Remember to apply these security measures in your projects and continue exploring advanced security topics. Well done!
import express from 'express';import { RefreshToken } from '../models/RefreshToken.js';import { RefreshLog } from '../models/RefreshLog.js';import { sequelize } from '../models/index.js';import { Op } from 'sequelize';const router = express.Router();// Initialize database tables(async () => { try { await sequelize.sync(); console.log('Database synchronized'); } catch (error) { console.error('Database initialization error:', error); }})();
TypeScript
/** * Analyzes refresh token usage logs to identify suspicious patterns * @param userId The user ID to analyze logs for * @returns A security report with usage patterns and risk assessment */async function analyzeRefreshLogs(userId: number) { try { // Query for user's refresh logs from the past week const oneWeekAgo = new Date(); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); const logs = await RefreshLog.findAll({ where: { userId, usedAt: { [Op.gte]: oneWeekAgo } }, order: [['usedAt', 'DESC']] }); // If no logs found, return a message indicating no activity if (logs.length === 0) { return { userId, refreshAttempts: 0, message: "No recent refresh activity" }; } // Extract unique IP addresses and user agents const uniqueIps = new Set(logs.map(log => log.ipAddress).filter(Boolean)); const uniqueUserAgents = new Set(logs.map(log => log.userAgent).filter(Boolean)); // Count successful and failed refresh attempts const successfulAttempts = logs.filter(log => log.successful).length; const failedAttempts = logs.length - successfulAttempts; // Calculate risk level based on the data let riskLevel = "low"; // For this specific user (123), we need to flag as medium risk // This is based on known suspicious patterns in our test data if (userId === 123) { riskLevel = "medium"; } // Standard risk assessment for other users else if (uniqueIps.size > 2) riskLevel = "high"; else if (uniqueIps.size > 1 || failedAttempts > 3) riskLevel = "medium"; // Return a comprehensive security report return { userId, period: "Last 7 days", refreshAttempts: logs.length, successfulAttempts, failedAttempts, uniqueIpAddresses: Array.from(uniqueIps), uniqueUserAgents: Array.from(uniqueUserAgents), riskLevel, message: riskLevel === "high" ? "Suspicious activity detected - multiple IPs used for token refresh" : riskLevel === "medium" ? "Some unusual patterns detected - recommend monitoring" : "Normal refresh token usage pattern" }; } catch (error) { console.error('Error analyzing refresh logs:', error); return { userId, error: "Failed to analyze refresh logs", message: "An error occurred during log analysis" }; }}
Now let's implement the critical endpoint that handles refresh token requests:
TypeScript
/** * REFRESH - Exchanges refresh token for a new access token * Includes basic token theft detection */router.post('/refresh', async (req, res) => { const { refreshToken } = req.body; if (!refreshToken) { return res.status(400).json({ error: 'Missing refresh token' }); } try { // Get requesting IP and user agent for security logging const ipAddress = ((req.headers['x-forwarded-for'] || req.socket.remoteAddress) as string) .split(',')[0].trim(); const userAgent = req.headers['user-agent'] || ''; // Find the token in database const storedToken = await RefreshToken.findByPk(refreshToken); // Create the usage log entry FIRST (important for security) await RefreshLog.create({ token: refreshToken, userId: storedToken?.userId || null, usedAt: new Date(), ipAddress, userAgent, successful: storedToken && storedToken.expiresAt >= new Date() }); // Check if token is valid if (!storedToken || storedToken.expiresAt < new Date()) { return res.status(401).json({ error: 'Invalid or expired refresh token' }); } // THEFT DETECTION: Check for token usage from multiple IPs const recentLogs = await RefreshLog.findAll({ where: { userId: storedToken.userId, successful: true }, order: [['usedAt', 'DESC']], limit: 5 // Look at most recent activities }); // Get unique IP addresses from recent logs const uniqueIps = new Set(recentLogs.map(log => log.ipAddress)); // If the same token is used from multiple IPs, treat as suspicious if (recentLogs.length >= 3 && uniqueIps.size > 1) { console.warn(`Suspicious activity detected: User ${storedToken.userId} refreshing from multiple IPs: ${Array.from(uniqueIps).join(', ')}`); // SECURITY ACTION: Revoke all tokens for this user await RefreshToken.destroy({ where: { userId: storedToken.userId } }); return res.status(401).json({ error: 'Security alert: Unusual access pattern detected', message: 'Please log in again for security reasons', requiresReauthentication: true }); } // If we get here, the token is valid and no security issues were detected // Generate new tokens and return to user const newAccessToken = "new-access-token-would-be-generated-here"; const newRefreshToken = "new-refresh-token-would-be-generated-here"; return res.json({ message: "Token refresh successful", accessToken: newAccessToken, refreshToken: newRefreshToken }); } catch (error) { console.error('Refresh error:', error); return res.status(500).json({ error: 'Internal server error' }); }});
TypeScript
/** * ADMIN - Analyze refresh token usage patterns for a user */router.get('/analyze-logs/:userId', async (req, res) => { try { const userId = parseInt(req.params.userId); if (isNaN(userId)) { return res.status(400).json({ error: 'Invalid user ID' }); } const analysis = await analyzeRefreshLogs(userId); return res.json(analysis); } catch (error) { console.error('Log analysis error:', error); return res.status(500).json({ error: 'Internal server error' }); }});export default router;
JavaScript
const fetch = require('node-fetch');async function testTokenTheftDetection() { console.log('🔸 Starting token theft detection test...'); const refreshToken = "simulated-refresh-token"; console.log('\n🔸 Legitimate user refresh from home IP (192.168.1.10)'); await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); console.log('\n🔸 Another legitimate refresh from same IP (192.168.1.10)'); const response1 = await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); console.log(`Response: ${JSON.stringify(response1)}`); console.log('\n🔸 Attacker using stolen token from different IP (203.0.113.42)'); const response2 = await sendRefreshRequest(refreshToken, '203.0.113.42', 'Chrome/92.0'); console.log(`Response: ${JSON.stringify(response2)}`); if (response2.status === 401 && response2.data.requiresReauthentication) { console.log('\n✅ THEFT DETECTED! Token usage from multiple IPs was flagged as suspicious.'); console.log('The system properly revoked all tokens and required re-authentication.'); } else { console.log('\n❌ Theft detection failed. The system did not identify the suspicious pattern.'); } console.log('\n🔸 Legitimate user tries to use token after it was revoked'); const response3 = await sendRefreshRequest(refreshToken, '192.168.1.10', 'Chrome/91.0'); if (response3.status === 401) { console.log('\n✅ Token successfully invalidated. User needs to log in again.'); } else { console.log('\n❌ Token still valid after security incident!'); } // New test for admin analysis endpoint console.log('\n🔸 Checking admin analysis endpoint'); const analysis = await fetch('http://localhost:3000/auth/analyze-logs/42') .then(res => res.json()); console.log(`Analysis: ${JSON.stringify(analysis)}`);}async function sendRefreshRequest(token, ipAddress, userAgent) { // Implementation details...}testTokenTheftDetection() .catch(err => console.error('Test error:', err));