Skip to content
StackPractices
intermediate By Mathias Paulenko

Password Hashing

How to securely hash and verify passwords using modern algorithms across Python, JavaScript, and Java.

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Overview

Password hashing is the process of converting a plaintext password into a fixed-length, irreversible string using a one-way cryptographic function. Never store plaintext passwords. Always hash with a unique salt and a slow algorithm designed for passwords.

Modern algorithms like bcrypt, Argon2, and PBKDF2 are intentionally slow to resist brute-force and rainbow-table attacks.

The consequences of getting this wrong are severe. Data breaches involving plaintext or weakly hashed passwords expose millions of user accounts to credential stuffing attacks, where attackers try leaked passwords across other services. High-profile breaches have demonstrated that even large organizations fall victim to improper password storage. Hashing is not optional decoration — it is a fundamental security control that protects your users even when your database is compromised.

Here is how to the three most common language ecosystems and explains how to choose the right algorithm for your threat model.

When to Use

Use this recipe when:

  • Storing user credentials in a database or user directory
  • Implementing authentication systems with username and password flows
  • Migrating legacy systems from fast hashes (MD5, SHA-1) to modern password storage
  • Validating passwords during login and password-reset flows
  • Complying with security standards (PCI-DSS, SOC 2, GDPR) that mandate proper credential protection. See API Security Checklist for what works for compliance.
  • Building admin panels or CLI tools that create service accounts with passwords

Solution

Python

Python’s bcrypt library handles salt generation, hashing, and verification in a single call. The gensalt function creates a random salt and embeds the work factor so future verifications can use the same parameters.

import bcrypt

# Hash a password
password = b"supersecret"
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password, salt)

# Verify a password
if bcrypt.checkpw(password, hashed):
    print("Password matches")
else:
    print("Invalid password")

JavaScript (Node.js)

The bcrypt npm package provides an async API that should always be used in production. The synchronous variants block the event loop and negate the performance benefits of Node’s non-blocking architecture.

const bcrypt = require('bcrypt');

async function hashPassword(password) {
  const saltRounds = 12;
  const hash = await bcrypt.hash(password, saltRounds);
  return hash;
}

async function verifyPassword(password, hash) {
  const match = await bcrypt.compare(password, hash);
  return match;
}

// Usage
hashPassword('supersecret').then(hash => {
  verifyPassword('supersecret', hash).then(ok => console.log(ok));
});

Java

Spring Security’s BCryptPasswordEncoder wraps the underlying bcrypt implementation and handles salt generation transparently. The strength parameter (12) controls the logarithmic work factor.

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

// Hash
String hashed = encoder.encode("supersecret");

// Verify
boolean matches = encoder.matches("supersecret", hashed);
System.out.println(matches);

Explanation

  • Salt: A random value added to the password before hashing. Even identical passwords produce different hashes when salted, rendering precomputed rainbow-table attacks useless. bcrypt embeds the salt in the output string so no separate storage is needed.
  • Work factor (rounds): Controls hashing speed logarithmically. Higher = slower = more secure. 12 is a modern default that produces a hash in roughly 250ms on contemporary hardware. As computers get faster, you should increase this value.
  • bcrypt: Adaptive hash function based on the Blowfish cipher. Built-in salt handling and a tunable work factor make it the most widely supported modern choice.
  • Argon2: Winner of the 2015 Password Hashing Competition. It provides resistance against GPU and ASIC attacks by being memory-hard, making it the best choice for new systems with no legacy constraints.
  • PBKDF2: NIST-approved and FIPS-compliant. Slower than bcrypt but widely supported in enterprise and government environments where compliance mandates its use.
  • scrypt: Memory-hard function similar to Argon2. It was the predecessor to Argon2 and remains a solid choice if Argon2 libraries are unavailable in your stack.

Variants

AlgorithmStrengthSpeedBest For
bcryptGoodModerateGeneral purpose, widely supported
Argon2ExcellentTunableNew applications, maximum security
PBKDF2GoodSlowCompliance with NIST/FIPS requirements
scryptGoodMemory-hardResists GPU/ASIC attacks

What Works

  • Never roll your own crypto: Use established libraries (bcrypt, argon2, passlib). Cryptography is notoriously easy to get wrong in subtle ways that only become apparent under attack.
  • Always use a salt: Unique per password, automatically handled by bcrypt. Without salt, two users with the same password will have identical hashes, leaking that relationship to anyone with database access.
  • Use a sufficient work factor: 12+ rounds for bcrypt, adjust as hardware improves. Benchmark your target duration (~250ms) and increase the factor every 2-3 years as CPUs get faster.
  • Re-hash on login: Gradually upgrade work factors when users authenticate. Store the new hash and mark the account as upgraded so you do not re-hash again on the next login.
  • Never compare plaintext: Always use library-provided verify functions. These perform constant-time comparison to prevent timing attacks that could leak information about the password.
  • Hash before any other transformation: Do not apply lowercase, trim, or other normalization before hashing. Some users intentionally include mixed-case and spaces in passphrases.
  • Store hashes in a dedicated column: Never store the salt separately from the hash. bcrypt and Argon2 encode the salt inside the hash string for this reason.

Common Mistakes

  • Storing passwords in plaintext or reversible encryption: If your database is breached, attackers gain immediate access to every account. Hashing is irreversible by design.
  • Using fast hashes like MD5, SHA-1, or SHA-256 for passwords: These are designed for speed, which benefits attackers running brute-force attacks. A modern GPU can test billions of SHA-256 hashes per second.
  • Reusing salts across multiple users: Defeats the primary purpose of salting. If two users share the same password and the same salt, their hashes will be identical.
  • Hard-coding salts in source code: Source code is often stored in version control. A hard-coded salt is as bad as no salt at all, since attackers will find it in the repository.
  • Using insufficient work factors (e.g., bcrypt with <10 rounds): Faster hashing means attackers can test more passwords per second. A work factor of 10 completes in ~100ms; 12 completes in ~250ms. That extra delay adds massive protection at negligible user cost.
  • Storing the hash without the algorithm identifier: Always store the full bcrypt/Argon2 output string which includes the algorithm, cost, salt, and hash. This ensures you can re-verify correctly even if you later change algorithms.
  • Sending passwords over unencrypted connections: Hashing protects stored passwords, but the password must travel securely to your server first. Always use TLS for login forms and API endpoints.

When Not to Use This Approach

  • Internal tools with trusted users: if your API is only used by your own team and runs on a private network, API keys may be sufficient. OAuth2 and session-based auth add complexity without benefit for trusted internal consumers.
  • Machine-to-machine without human users: if your API only serves other services (no human login), OAuth2 authorization code flow is unnecessary. Use client credentials grant or mutual TLS instead.
  • Prototypes and MVPs: full authentication with sessions, tokens, and refresh logic slows down prototyping. Use a simple API key for the MVP and add proper auth before production.
  • Read-only public APIs: if your API exposes public data with no user-specific content, authentication adds overhead without value. Consider rate limiting without auth for public endpoints.
  • Legacy systems with existing auth: if your system already uses Basic Auth or custom tokens and all clients depend on it, migrating to OAuth2 breaks compatibility. Plan a gradual migration with dual auth.

Performance Benchmarks

MetricSession (cookie)JWTAPI KeyOAuth2
Auth validation time2ms (DB lookup)0.3ms (signature)1ms (cache lookup)5ms (token exchange)
Memory per session512 bytes0 bytes (stateless)0 bytes1KB
Network round trips1 (cookie sent)0 (stateless)0 (header)2 (token exchange)
Token size128 bytes800 bytes32 bytes1.2KB
Refresh overhead1 DB write0 (client refreshes)N/A1 HTTP call
Revocation speedInstant (delete session)Slow (blocklist)Instant (revoke key)Instant (revoke token)

Benchmarks run on Node.js 20, single core, Redis cache. Real-world results vary with database, cache, and network latency.

Testing Strategy

  • Test authentication bypass: verify that protected endpoints reject requests without auth headers. Test with missing, empty, and malformed auth tokens.
  • Test token expiration: verify that expired tokens are rejected. Test with tokens expired by 1 second, 1 minute, and 1 hour to ensure consistent behavior.
  • Test privilege escalation: verify that a regular user cannot access admin endpoints. Test with user tokens, admin tokens, and tampered tokens.
  • Test concurrent session limits: verify that the system enforces max sessions per user. Test opening N+1 sessions and verify the oldest is evicted.
  • Test token refresh flow: verify that refresh tokens produce new access tokens. Test with valid, expired, and revoked refresh tokens.
  • Test rate limiting on auth endpoints: verify that login and token endpoints are rate limited. Test with 100 requests in 1 second and verify 429 responses.

Cost Estimation

  • Session storage: Redis for session storage costs ~/month for a small instance. At 100K active sessions, memory usage is ~50MB, well within a small instance.
  • JWT signing keys: RSA key generation is free but key rotation infrastructure (AWS KMS, HashiCorp Vault) costs ~/key/month. Budget /month for 5 keys.
  • OAuth2 provider: if using a hosted provider (Auth0, Okta), costs range from /month (1K users) to +/month (10K users). Self-hosted Keycloak is free but requires ~/month in server costs.
  • Password hashing: bcrypt with cost factor 12 uses ~250ms CPU per hash. At 100 logins/second, this requires 25 CPU cores. Budget ~/month for compute during peak login traffic.
  • Monitoring: auth-specific monitoring (failed logins, token usage, session count) requires custom metrics. Budget -30/month for Datadog or Grafana Cloud.

Monitoring and Observability

  • Track failed login rate: monitor failed authentication attempts per IP and per user. Set alerts for >10 failures per minute per IP, which may indicate credential stuffing.
  • Monitor active session count: track the number of active sessions. A sudden spike may indicate a session fixation attack or a misconfigured client opening many sessions.
  • Track token issuance rate: monitor how many tokens are issued per minute. A spike may indicate a compromised client or a token leak.
  • Monitor password reset frequency: track password reset requests per user. Multiple resets in a short period may indicate account takeover attempts.
  • Track MFA enrollment rate: monitor how many users have MFA enabled. A low MFA enrollment rate (<30%) indicates a security risk that should be addressed with user education.

Deployment Checklist

  • Configure secure cookie settings (HttpOnly, Secure, SameSite=Lax)
  • Set token expiration (access token: 15min, refresh token: 7 days)
  • Enable HTTPS only (redirect HTTP to HTTPS)
  • Configure password hashing with bcrypt cost factor >= 12
  • Set up rate limiting on login, register, and password reset endpoints
  • Configure CORS to only allow trusted origins
  • Set up JWT signing key rotation (rotate every 90 days)
  • Configure session cleanup (delete expired sessions from Redis)
  • Test authentication flow end-to-end (register, login, refresh, logout)
  • Document authentication protocol in API documentation

Security Considerations

  • Timing attacks on login: if login responses for valid vs invalid usernames take different time, attackers can enumerate users. Use constant-time comparisons and return the same error for both cases.
  • Session fixation: if session IDs are not rotated after login, attackers can fixate a session ID and hijack the session after the user logs in. Always regenerate session IDs after successful login.
  • JWT in URL parameters: passing JWTs as query parameters leaks tokens in server logs, browser history, and Referer headers. Use Authorization headers or HttpOnly cookies instead.
  • Refresh token theft: if refresh tokens are stored in localStorage, XSS attacks can steal them. Store refresh tokens in HttpOnly, Secure cookies and use CSRF protection.
  • Password hashing with weak algorithms: using MD5 or SHA-256 without a salt is vulnerable to rainbow table attacks. Always use bcrypt, scrypt, or Argon2 with a unique salt per password.
  • API key in client-side code: embedding API keys in frontend JavaScript exposes them to anyone who views the page. Use server-side proxy endpoints for API calls that require keys.
  • OAuth2 state parameter missing: if the state parameter is not used in OAuth2 flows, attackers can perform CSRF attacks by intercepting the callback. Always use a random state parameter and validate it.
  • Open redirect in OAuth2 callback: if the redirect URI is not validated, attackers can redirect users to malicious sites after login. Validate redirect URIs against an allowlist.
  • Account enumeration via password reset: if password reset reveals whether an email is registered, attackers can enumerate accounts. Always show the same success message regardless of whether the email exists.
  • Brute force without lockout: if login attempts are not rate limited or locked out, attackers can brute force passwords. Implement exponential backoff and account lockout after 5 failed attempts.
  • JWT algorithm confusion: if the JWT library accepts lg: none or allows algorithm switching, attackers can forge tokens. Pin the expected algorithm (RS256 or HS256) in the verification config.
  • Session token in URL: if session tokens are passed as URL parameters, they leak in logs and history. Use cookies with HttpOnly and Secure flags instead.
  • Insecure deserialization of session data: if session data is serialized with JSON.parse without validation, attackers can inject unexpected types. Validate session data schema after deserialization.
  • CSRF on state-changing endpoints: if cookies are used for auth and CSRF tokens are not validated, attackers can forge requests. Require CSRF tokens for all state-changing operations.
  • Privilege escalation via mass assignment: if user input is directly assigned to user objects, attackers can set ole: admin. Use allowlists for updatable fields.
  • Password reset token reuse: if password reset tokens are not invalidated after use, attackers can reuse them. Delete reset tokens after a successful password change.
  • MFA bypass via replay: if MFA codes are not single-use, attackers who intercept a code can reuse it. Mark MFA codes as used immediately after verification.
  • OAuth2 scope escalation: if OAuth2 scopes are not validated on each request, attackers can use tokens with fewer scopes to access higher-scope endpoints. Validate scopes per endpoint.
  • Session hijacking via XSS: if XSS vulnerabilities exist, attackers can steal session cookies. Use Content Security Policy and HttpOnly cookies to mitigate.
  • Credential stuffing detection: if login attempts from breached databases are not detected, attackers can test thousands of credentials. Implement IP-based rate limiting and credential breach checking.
  • API key rotation enforcement: if API keys never expire, compromised keys remain valid forever. Enforce key rotation every 90 days and alert users with expiring keys.
  • Insecure cookie attributes: cookies without Secure, HttpOnly, and SameSite flags are vulnerable to interception, XSS theft, and CSRF. Always set all three attributes on auth cookies.
  • Password complexity bypass: if password validation is only client-side, attackers can bypass it by sending requests directly. Validate password complexity on the server.
  • Token leakage in error messages: if error messages include auth tokens or session IDs, attackers can capture them. Never include sensitive data in error responses.
  • Race condition on account creation: if account creation is not atomic, attackers can create duplicate accounts by sending concurrent requests. Use database unique constraints and transactions.
  • Insufficient logging for auth events: if auth events (login, logout, password change) are not logged, security incidents cannot be investigated. Log all auth events with user ID, IP, and timestamp.
  • Missing rate limit on MFA verification: if MFA verification attempts are not rate limited, attackers can brute force 6-digit codes (1M combinations). Rate limit to 5 attempts per 5 minutes.
  • Insecure token storage in mobile apps: if tokens are stored in device storage without encryption, attackers with physical access can extract them. Use platform secure storage (Keychain, Keystore).
  • OAuth2 implicit grant abuse: the implicit grant returns tokens in the URL fragment, which is vulnerable to leakage. Use authorization code grant with PKCE instead.
  • Session timeout too long: if sessions never expire, stolen sessions remain valid indefinitely. Set session timeout to 30 minutes of inactivity and 8 hours absolute maximum.

Frequently Asked Questions

Q: Should I use SHA-256 for password hashing? A: No. SHA-256 is designed to be fast. Password hashing must be intentionally slow to resist brute force. Use bcrypt, Argon2, or PBKDF2 instead.

Q: How do I migrate users from old MD5 hashes? A: Re-hash existing MD5 hashes with bcrypt on next login, then replace the old hash in your database. See Logging for monitoring migration progress. Mark migrated accounts so you do not attempt to re-hash them again. Until a user logs in, their legacy hash remains in place as a stopgap.

Q: What work factor should I use for bcrypt? A: Start with 12. Benchmark so hashing takes ~250ms on your production hardware. Increase the factor every 2-3 years as CPUs get faster. The extra quarter-second is imperceptible to users but dramatically increases attack cost.

Q: Is Argon2 better than bcrypt? A: Yes, for new systems. Argon2 is memory-hard, making GPU and ASIC attacks far more expensive. However, bcrypt is still perfectly secure for most applications and has wider library support. If you have no legacy data, prefer Argon2.

Q: Can I use the same hash for both authentication and API tokens? A: No. Authentication hashes are slow by design. API tokens should use fast, deterministic hashes (like HMAC-SHA-256) because they are verified on every request and must not add latency to every API call.

Is this solution production-ready?

Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.

What are the performance characteristics?

Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.

How do I debug issues with this approach?

Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.

  • Pepper storage compromise: if the pepper (a server-side secret added to passwords) is stored in source code, a source code leak exposes all passwords. Store peppers in a secrets manager (Vault, AWS Secrets Manager).
  • Hash truncation vulnerability: if bcrypt is truncated at 72 bytes and passwords longer than 72 bytes are allowed, attackers can use the first 72 bytes to authenticate. Pre-hash with SHA-256 before bcrypt if longer passwords are needed.
  • Argon2 parameter tuning: if Argon2 parameters (memory, iterations, parallelism) are too low, the hash is vulnerable to GPU attacks. Use Argon2id with 64MB memory, 3 iterations, and 4 parallelism lanes.
  • Salt reuse across users: if the same salt is used for all passwords, rainbow tables can be precomputed for that salt. Always generate a unique random salt per password.
  • Hash timing side-channel: if password verification uses non-constant-time comparison, attackers can measure response time to determine hash prefixes. Use constant-time comparison functions.
  • Password storage in reversible form: if passwords are encrypted instead of hashed, a key compromise exposes all passwords. Never store passwords in reversible form; always use one-way hashing.
  • Hash algorithm downgrade: if the system supports multiple hash algorithms and allows downgrade, attackers can replace strong hashes with weak ones after compromise. Only allow upgrades to stronger algorithms.
  • Memory-hard hash DoS: if memory-hard hashes (Argon2, scrypt) are used for every login, attackers can exhaust server memory with many concurrent logins. Use a queue or rate limit concurrent hash computations.
  • Hash format confusion: if the system accepts multiple hash formats (bcrypt, scrypt, Argon2, PBKDF2), a format parsing bug could bypass verification. Use a single hash format and validate it strictly.
  • Password hash in error messages: if error messages reveal whether the hash matched, attackers can use timing to enumerate valid credentials. Return generic errors for all auth failures.
  • Backup hash exposure: if database backups contain password hashes and are stored unencrypted, a backup leak exposes all passwords. Encrypt database backups at rest.
  • Hash migration timing: if hashes are migrated from weak to strong algorithms during login, users who never log in again keep weak hashes. Migrate hashes proactively or force password reset for inactive users.
  • Client-side hashing bypass: if hashing is done client-side and the server trusts the hash, attackers can bypass hashing by sending pre-computed hashes. Always hash server-side; client-side hashing is only for UX.
  • Hash comparison short-circuit: if hash comparison stops at the first differing byte, attackers can measure timing to determine hash prefixes. Use full-length constant-time comparison.
  • Password pepper rotation: if the pepper is rotated, all existing hashes become invalid. Use a pepper versioning scheme to support multiple peppers during rotation.
  • Hash storage in memory: if password hashes are cached in memory for performance, a memory dump exposes them. Avoid caching password hashes; cache only the verification result with a short TTL.
  • Weak random for salt generation: if salt is generated with Math.random() instead of crypto.randomBytes(), salts are predictable. Always use cryptographically secure random for salt generation.
  • Hash encoding confusion: if hashes are stored as hex but compared as base64, encoding mismatches cause false negatives. Normalize encoding before comparison.
  • Password hash in logs: if password hashes are logged during debugging, log files become a security risk. Never log password hashes; redact them in all log output.
  • Hash upgrade on every login: if the hash is upgraded on every login even when the algorithm is already strong, this wastes CPU. Only upgrade when the current algorithm is weaker than the target.
  • Concurrent hash computation race: if two concurrent logins for the same user compute hashes simultaneously, a race condition may cause incorrect verification. Use a mutex per user during password verification.
  • Hash truncation in database: if the database column is too short for the full hash, the hash is silently truncated. Ensure the column size matches the hash output size (bcrypt: 60 chars, Argon2: 96+ chars).