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 password handling in TypeScript! ๐ In this guide, weโll explore how to securely handle passwords using hashing and salting techniques.
Youโll discover how proper password handling can protect your usersโ accounts and keep your applications secure. Whether youโre building web applications ๐, APIs ๐ฅ๏ธ, or authentication systems ๐, understanding password security is essential for writing robust, secure code.
By the end of this tutorial, youโll feel confident implementing secure password handling in your own projects! Letโs dive in! ๐โโ๏ธ
๐ Understanding Password Hashing and Salting
๐ค What is Password Hashing?
Password hashing is like a one-way meat grinder ๐ฅฉ. Think of it as a magical blender that turns your password into an unrecognizable smoothie - you canโt turn the smoothie back into the original ingredients!
In TypeScript terms, hashing transforms a password into a fixed-length string of characters that cannot be reversed. This means you can:
- โจ Store passwords safely without knowing the actual password
- ๐ Verify passwords without storing them in plain text
- ๐ก๏ธ Protect user data even if your database is compromised
๐ก Why Use Salting?
Hereโs why developers love salting:
- Rainbow Table Protection ๐: Prevents pre-computed hash attacks
- Unique Hashes ๐ป: Same password gets different hashes for different users
- Extra Security Layer ๐: Makes brute force attacks much harder
- Industry Standard ๐ง: Required for compliance with security standards
Real-world example: Imagine building a user authentication system ๐ค. With proper hashing and salting, even if someone steals your database, they canโt recover the original passwords!
๐ง Basic Syntax and Usage
๐ Simple Example
Letโs start with a friendly example using bcrypt:
// ๐ Hello, secure passwords!
import bcrypt from 'bcryptjs';
// ๐จ Creating a password hasher
interface UserCredentials {
email: string; // ๐ง User's email
password: string; // ๐ Plain text password (never store this!)
salt?: string; // ๐ง Optional salt for extra flavor
}
// ๐ Hash a password
async function hashPassword(password: string): Promise<string> {
const saltRounds = 10; // ๐ฏ Cost factor
const hashedPassword = await bcrypt.hash(password, saltRounds);
return hashedPassword;
}
// โ
Verify a password
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
๐ก Explanation: Notice how we never store the plain password! The salt is automatically generated and embedded in the hash by bcrypt.
๐ฏ Common Patterns
Here are patterns youโll use daily:
// ๐๏ธ Pattern 1: User registration
interface User {
id: string;
email: string;
passwordHash: string; // ๐ Never store plain passwords!
}
async function registerUser(email: string, password: string): Promise<User> {
// ๐จ Hash the password
const passwordHash = await hashPassword(password);
// ๐พ Save to database
const user: User = {
id: Date.now().toString(),
email,
passwordHash
};
console.log(`โ
User ${email} registered successfully!`);
return user;
}
// ๐ Pattern 2: User login
async function loginUser(email: string, password: string, user: User): Promise<boolean> {
// ๐ Verify the password
const isValid = await verifyPassword(password, user.passwordHash);
if (isValid) {
console.log(`๐ Welcome back, ${email}!`);
return true;
}
console.log(`โ Invalid credentials!`);
return false;
}
// ๐จ Pattern 3: Password strength validation
function validatePasswordStrength(password: string): { valid: boolean; message: string; emoji: string } {
if (password.length < 8) {
return { valid: false, message: "Too short!", emoji: "๐ฐ" };
}
if (!/[A-Z]/.test(password) || !/[a-z]/.test(password) || !/[0-9]/.test(password)) {
return { valid: false, message: "Add uppercase, lowercase, and numbers!", emoji: "๐ค" };
}
return { valid: true, message: "Strong password!", emoji: "๐ช" };
}
๐ก Practical Examples
๐ Example 1: E-commerce User System
Letโs build something real:
// ๐๏ธ Define our secure user system
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
interface SecureUser {
id: string;
username: string;
email: string;
passwordHash: string;
createdAt: Date;
lastLogin?: Date;
failedAttempts: number;
}
// ๐ User authentication service
class AuthService {
private users: Map<string, SecureUser> = new Map();
private readonly MAX_ATTEMPTS = 5; // ๐ซ Lock after 5 failed attempts
// ๐ Register new user
async register(username: string, email: string, password: string): Promise<SecureUser | null> {
// ๐ก Check password strength
const strength = this.checkPasswordStrength(password);
if (!strength.valid) {
console.log(`โ Weak password: ${strength.message} ${strength.emoji}`);
return null;
}
// ๐จ Generate secure hash
const saltRounds = 12; // ๐ Higher = more secure but slower
const passwordHash = await bcrypt.hash(password, saltRounds);
// ๐ค Create user
const user: SecureUser = {
id: crypto.randomUUID(),
username,
email,
passwordHash,
createdAt: new Date(),
failedAttempts: 0
};
this.users.set(email, user);
console.log(`๐ Welcome ${username}! Your account is secured with military-grade encryption ๐ก๏ธ`);
return user;
}
// ๐ Authenticate user
async login(email: string, password: string): Promise<{ success: boolean; user?: SecureUser; message: string }> {
const user = this.users.get(email);
if (!user) {
// ๐ต๏ธ Don't reveal if user exists
await this.fakeHashDelay(); // Prevent timing attacks
return { success: false, message: "Invalid credentials ๐ซ" };
}
// ๐ซ Check if account is locked
if (user.failedAttempts >= this.MAX_ATTEMPTS) {
return { success: false, message: "Account locked! Contact support ๐" };
}
// โ
Verify password
const isValid = await bcrypt.compare(password, user.passwordHash);
if (isValid) {
user.lastLogin = new Date();
user.failedAttempts = 0;
console.log(`๐ Welcome back ${user.username}!`);
return { success: true, user, message: "Login successful! ๐" };
}
// โ Invalid password
user.failedAttempts++;
const remaining = this.MAX_ATTEMPTS - user.failedAttempts;
return {
success: false,
message: `Invalid credentials. ${remaining} attempts remaining โ ๏ธ`
};
}
// ๐ช Check password strength
private checkPasswordStrength(password: string): { valid: boolean; message: string; emoji: string } {
const checks = [
{ test: password.length >= 8, message: "8+ characters", emoji: "๐" },
{ test: /[A-Z]/.test(password), message: "uppercase letter", emoji: "๐ " },
{ test: /[a-z]/.test(password), message: "lowercase letter", emoji: "๐ก" },
{ test: /[0-9]/.test(password), message: "number", emoji: "๐ข" },
{ test: /[^A-Za-z0-9]/.test(password), message: "special character", emoji: "โจ" }
];
const failed = checks.filter(check => !check.test);
if (failed.length === 0) {
return { valid: true, message: "Super strong password!", emoji: "๐ช" };
}
const missing = failed.map(f => f.message).join(", ");
return { valid: false, message: `Add: ${missing}`, emoji: "๐ค" };
}
// ๐ Fake delay to prevent timing attacks
private async fakeHashDelay(): Promise<void> {
const fakePassword = "dummy_password_for_timing";
await bcrypt.hash(fakePassword, 10);
}
}
// ๐ฎ Let's use it!
const auth = new AuthService();
await auth.register("CodeNinja", "[email protected]", "SuperSecret123!");
await auth.login("[email protected]", "SuperSecret123!");
๐ฏ Try it yourself: Add a password reset feature with secure tokens!
๐ฎ Example 2: Game Account Security
Letโs make it fun:
// ๐ Secure game account system
interface GameAccount {
playerId: string;
username: string;
passwordHash: string;
securityQuestions: SecurityQuestion[];
twoFactorEnabled: boolean;
achievements: string[];
}
interface SecurityQuestion {
question: string;
answerHash: string; // ๐ Hash the answers too!
}
class GameSecurity {
private accounts: Map<string, GameAccount> = new Map();
// ๐ฎ Create secure game account
async createAccount(username: string, password: string): Promise<GameAccount> {
// ๐ฒ Generate unique player ID
const playerId = `PLAYER_${Date.now()}_${Math.random().toString(36).substring(7)}`;
// ๐ Hash with extra security for gamers
const saltRounds = 14; // ๐ช Gamers deserve extra security!
const passwordHash = await bcrypt.hash(password, saltRounds);
const account: GameAccount = {
playerId,
username,
passwordHash,
securityQuestions: [],
twoFactorEnabled: false,
achievements: ["๐ First Steps", "๐ก๏ธ Security Conscious"]
};
this.accounts.set(username, account);
console.log(`๐ฎ Player ${username} joined the game! Achievement unlocked: ${account.achievements[0]}`);
return account;
}
// ๐ Add security question
async addSecurityQuestion(username: string, question: string, answer: string): Promise<void> {
const account = this.accounts.get(username);
if (!account) return;
// ๐จ Hash the answer (normalize to lowercase first)
const normalizedAnswer = answer.toLowerCase().trim();
const answerHash = await bcrypt.hash(normalizedAnswer, 10);
account.securityQuestions.push({ question, answerHash });
account.achievements.push("๐ Extra Secure");
console.log(`โ
Security question added! You earned: ๐ Extra Secure`);
}
// ๐ฏ Verify security answer
async verifySecurityAnswer(
username: string,
questionIndex: number,
answer: string
): Promise<boolean> {
const account = this.accounts.get(username);
if (!account || !account.securityQuestions[questionIndex]) {
return false;
}
const normalizedAnswer = answer.toLowerCase().trim();
const question = account.securityQuestions[questionIndex];
const isValid = await bcrypt.compare(normalizedAnswer, question.answerHash);
if (isValid) {
console.log(`โ
Security answer correct! ๐`);
return true;
}
console.log(`โ Wrong answer! Try again ๐ค`);
return false;
}
// ๐ Change password securely
async changePassword(
username: string,
oldPassword: string,
newPassword: string
): Promise<boolean> {
const account = this.accounts.get(username);
if (!account) return false;
// ๐ Verify old password
const isValid = await bcrypt.compare(oldPassword, account.passwordHash);
if (!isValid) {
console.log(`โ Current password incorrect!`);
return false;
}
// ๐ช Check new password isn't the same
const isSame = await bcrypt.compare(newPassword, account.passwordHash);
if (isSame) {
console.log(`โ ๏ธ New password must be different!`);
return false;
}
// ๐จ Hash new password
account.passwordHash = await bcrypt.hash(newPassword, 14);
account.achievements.push("๐ Password Champion");
console.log(`โ
Password changed successfully! Achievement unlocked: ๐ Password Champion`);
return true;
}
}
๐ Advanced Concepts
๐งโโ๏ธ Advanced Topic 1: Argon2 - The Gold Standard
When youโre ready to level up, try this advanced pattern:
// ๐ฏ Advanced password hashing with Argon2
import argon2 from 'argon2';
interface AdvancedHashOptions {
memoryCost?: number; // ๐พ Memory usage
timeCost?: number; // โฑ๏ธ Iterations
parallelism?: number; // ๐ Threads
hashLength?: number; // ๐ Output length
}
class AdvancedPasswordManager {
// ๐ช Hash with Argon2 (winner of Password Hashing Competition!)
async hashPasswordAdvanced(
password: string,
options?: AdvancedHashOptions
): Promise<string> {
const defaultOptions = {
memoryCost: 2 ** 16, // 64 MB
timeCost: 3, // 3 iterations
parallelism: 1, // 1 thread
hashLength: 32 // 32 bytes
};
const hash = await argon2.hash(password, {
...defaultOptions,
...options,
type: argon2.argon2id // ๐ก๏ธ Best for password hashing
});
console.log(`โจ Password hashed with military-grade Argon2!`);
return hash;
}
// ๐ Verify with timing attack protection
async verifyPasswordAdvanced(password: string, hash: string): Promise<boolean> {
try {
const isValid = await argon2.verify(hash, password);
return isValid;
} catch (error) {
// ๐ซ Hash format might be invalid
console.log(`โ ๏ธ Invalid hash format!`);
return false;
}
}
}
๐๏ธ Advanced Topic 2: Pepper and Additional Security
For the brave developers:
// ๐ Enterprise-level security with pepper
class EnterprisePasswordSecurity {
private readonly pepper: string; // ๐ถ๏ธ Secret server-side key
constructor(pepper: string) {
this.pepper = pepper;
}
// ๐ Hash with pepper (extra spicy security!)
async hashWithPepper(password: string): Promise<string> {
// ๐จ Add pepper to password
const pepperedPassword = password + this.pepper;
// ๐ง Generate salt and hash
const saltRounds = 12;
const hash = await bcrypt.hash(pepperedPassword, saltRounds);
return hash;
}
// โ
Verify peppered password
async verifyWithPepper(password: string, hash: string): Promise<boolean> {
const pepperedPassword = password + this.pepper;
return await bcrypt.compare(pepperedPassword, hash);
}
// ๐ฏ Password history check
async checkPasswordHistory(
newPassword: string,
previousHashes: string[]
): Promise<boolean> {
// ๐ Check against previous passwords
for (const oldHash of previousHashes) {
const isSame = await this.verifyWithPepper(newPassword, oldHash);
if (isSame) {
console.log(`โ Password was used before! Choose a new one ๐`);
return false;
}
}
console.log(`โ
Password is unique! ๐`);
return true;
}
}
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Storing Plain Passwords
// โ Wrong way - NEVER do this!
interface InsecureUser {
username: string;
password: string; // ๐ฅ This is a security disaster!
}
const badUser: InsecureUser = {
username: "victim",
password: "myPassword123" // ๐ฐ Anyone can read this!
};
// โ
Correct way - always hash!
interface SecureUser {
username: string;
passwordHash: string; // ๐ก๏ธ Safe and secure!
}
async function createSecureUser(username: string, password: string): Promise<SecureUser> {
const passwordHash = await bcrypt.hash(password, 12);
return { username, passwordHash };
}
๐คฏ Pitfall 2: Using Weak Hashing Algorithms
// โ Dangerous - MD5/SHA1 are broken!
import crypto from 'crypto';
function badHash(password: string): string {
return crypto.createHash('md5').update(password).digest('hex'); // ๐ฅ Crackable in seconds!
}
// โ
Safe - use bcrypt, scrypt, or argon2!
async function goodHash(password: string): Promise<string> {
return await bcrypt.hash(password, 12); // โ
Secure and future-proof!
}
๐ ๏ธ Best Practices
- ๐ฏ Use Strong Algorithms: bcrypt, scrypt, or Argon2 - never MD5 or SHA1!
- ๐ Salt Everything: Always use unique salts (bcrypt does this automatically)
- ๐ก๏ธ Increase Work Factor: Use cost factor 12+ for bcrypt
- ๐จ Validate Password Strength: Enforce minimum requirements
- โจ Never Log Passwords: Not even for debugging!
๐งช Hands-On Exercise
๐ฏ Challenge: Build a Secure Password Reset System
Create a type-safe password reset system:
๐ Requirements:
- โ Generate secure reset tokens
- ๐ท๏ธ Token expiration (1 hour max)
- ๐ค Email verification before reset
- ๐ Track reset attempts
- ๐จ Prevent token reuse!
๐ Bonus Points:
- Add rate limiting for reset requests
- Implement magic link authentication
- Create password strength meter
๐ก Solution
๐ Click to see solution
// ๐ฏ Our secure password reset system!
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
interface ResetToken {
token: string;
email: string;
expiresAt: Date;
used: boolean;
}
interface PasswordResetRequest {
id: string;
email: string;
requestedAt: Date;
ipAddress: string;
}
class PasswordResetService {
private resetTokens: Map<string, ResetToken> = new Map();
private resetRequests: PasswordResetRequest[] = [];
private readonly TOKEN_EXPIRY_MINUTES = 60; // โฐ 1 hour
private readonly MAX_REQUESTS_PER_HOUR = 3; // ๐ซ Rate limiting
// ๐ฒ Generate secure reset token
generateResetToken(email: string): string | null {
// ๐ก๏ธ Check rate limit
if (!this.checkRateLimit(email)) {
console.log(`โ ๏ธ Too many reset requests! Try again later ๐`);
return null;
}
// ๐ฏ Generate cryptographically secure token
const token = crypto.randomBytes(32).toString('hex');
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
// ๐พ Store token details
const resetToken: ResetToken = {
token: hashedToken,
email,
expiresAt: new Date(Date.now() + this.TOKEN_EXPIRY_MINUTES * 60 * 1000),
used: false
};
this.resetTokens.set(hashedToken, resetToken);
// ๐ Log request
this.resetRequests.push({
id: crypto.randomUUID(),
email,
requestedAt: new Date(),
ipAddress: "127.0.0.1" // In real app, get from request
});
console.log(`๐ง Password reset token sent to ${email}!`);
return token; // Return unhashed token for email
}
// โ
Validate reset token
validateToken(token: string): { valid: boolean; email?: string; message: string } {
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const resetToken = this.resetTokens.get(hashedToken);
if (!resetToken) {
return { valid: false, message: "Invalid token ๐ซ" };
}
if (resetToken.used) {
return { valid: false, message: "Token already used ๐" };
}
if (new Date() > resetToken.expiresAt) {
return { valid: false, message: "Token expired โฐ" };
}
return {
valid: true,
email: resetToken.email,
message: "Token valid โ
"
};
}
// ๐ Reset password with token
async resetPassword(token: string, newPassword: string): Promise<boolean> {
const validation = this.validateToken(token);
if (!validation.valid) {
console.log(`โ Reset failed: ${validation.message}`);
return false;
}
// ๐ช Check password strength
const strength = this.checkPasswordStrength(newPassword);
if (!strength.valid) {
console.log(`โ Weak password: ${strength.message}`);
return false;
}
// ๐จ Hash new password
const hashedPassword = await bcrypt.hash(newPassword, 12);
// ๐ Mark token as used
const hashedToken = crypto
.createHash('sha256')
.update(token)
.digest('hex');
const resetToken = this.resetTokens.get(hashedToken)!;
resetToken.used = true;
console.log(`โ
Password reset successful for ${validation.email}! ๐`);
return true;
}
// ๐ฆ Rate limiting check
private checkRateLimit(email: string): boolean {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const recentRequests = this.resetRequests.filter(
req => req.email === email && req.requestedAt > oneHourAgo
);
return recentRequests.length < this.MAX_REQUESTS_PER_HOUR;
}
// ๐ช Password strength checker
private checkPasswordStrength(password: string): { valid: boolean; message: string } {
if (password.length < 8) {
return { valid: false, message: "Too short! Need 8+ characters ๐" };
}
const hasUpper = /[A-Z]/.test(password);
const hasLower = /[a-z]/.test(password);
const hasNumber = /[0-9]/.test(password);
const hasSpecial = /[^A-Za-z0-9]/.test(password);
const strength = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length;
if (strength < 3) {
return { valid: false, message: "Add more variety! ๐จ" };
}
return { valid: true, message: "Strong password! ๐ช" };
}
// ๐งน Clean expired tokens
cleanExpiredTokens(): void {
const now = new Date();
let cleaned = 0;
for (const [hash, token] of this.resetTokens) {
if (now > token.expiresAt || token.used) {
this.resetTokens.delete(hash);
cleaned++;
}
}
console.log(`๐งน Cleaned ${cleaned} expired tokens!`);
}
}
// ๐ฎ Test it out!
const resetService = new PasswordResetService();
const token = resetService.generateResetToken("[email protected]");
if (token) {
await resetService.resetPassword(token, "NewSecurePass123!");
}
๐ Key Takeaways
Youโve learned so much! Hereโs what you can now do:
- โ Implement secure password hashing with confidence ๐ช
- โ Avoid common security mistakes that compromise user data ๐ก๏ธ
- โ Apply best practices for password handling ๐ฏ
- โ Debug authentication issues like a pro ๐
- โ Build secure authentication systems with TypeScript! ๐
Remember: Security is not optional - itโs your responsibility to protect your usersโ data! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve mastered password hashing and salting!
Hereโs what to do next:
- ๐ป Practice with the exercises above
- ๐๏ธ Build a complete authentication system
- ๐ Move on to our next tutorial: JWT Implementation and Session Management
- ๐ Share your secure coding journey with others!
Remember: Every security expert was once a beginner. Keep coding, keep learning, and most importantly, keep your users safe! ๐
Happy secure coding! ๐๐โจ