How To Securely Hash Passwords In PHP?

Published October 4, 2024

Problem: Secure Password Hashing in PHP

Storing passwords securely is important for web application security. Hashing passwords helps protect user data from unauthorized access, even if the database is compromised. PHP developers need to use good hashing techniques to protect sensitive information.

Best Practices for Password Hashing in PHP

Using Modern Hashing Algorithms

When hashing passwords in PHP, use modern algorithms for security. Two good options are:

  • Bcrypt: A common choice for password hashing in PHP. It's slow and requires a lot of computing power, which makes it hard to crack. Bcrypt adds salt automatically for extra security.

  • Argon2: A newer option with more security features. It needs a lot of memory to work, which makes it even harder to crack. While not as common as Bcrypt, it's becoming more popular because it's very secure.

Tip: Choosing the Right Algorithm

When selecting between Bcrypt and Argon2, consider your server resources. If your server has limited memory, Bcrypt might be a better choice. For servers with ample memory, Argon2 offers enhanced security.

Implementing Salt in Password Hashing

Salt is important for password security:

  • Purpose and benefits: Salt is a random string added to the password before hashing. It stops attackers from using pre-made hash tables to crack passwords. Even if two users have the same password, their hashed passwords will be different because of the unique salt.

  • Generating secure salt in PHP: PHP's password_hash() function makes a secure salt automatically. If you need to make salt manually, use the random_bytes() function to create a random string. For example:

$salt = bin2hex(random_bytes(16));

This creates a 32-character hexadecimal salt. However, it's usually better to let PHP handle salt creation automatically when using functions like password_hash().

PHP's Built-in Password Hashing Functions

password_hash() Function

The password_hash() function is PHP's built-in tool for secure password hashing. It's simple to use and handles security details automatically.

Syntax and usage:

$hashedPassword = password_hash($password, PASSWORD_DEFAULT);

This function takes two main parameters:

  1. The password to hash
  2. The hashing algorithm to use (PASSWORD_DEFAULT is recommended)

The function returns a string with the hashed password.

Automatic salt generation: password_hash() creates a random salt for each password automatically. This removes the need for manual salt handling, reducing error risks.

Tip: Use Strong Passwords

Always encourage users to create strong passwords. Implement password strength checks in your application to ensure passwords meet minimum requirements, such as length and complexity.

password_verify() Function

The password_verify() function checks if a password matches a hash created by password_hash().

Verifying hashed passwords:

if (password_verify($userInputPassword, $storedHashedPassword)) {
    echo "Password is correct!";
} else {
    echo "Password is incorrect.";
}

This function compares the user-provided password with the stored hash.

Secure password comparison: password_verify() uses timing-safe comparison methods. This protects against timing attacks where attackers might try to guess passwords based on comparison time.

Using these built-in functions provides a simple and secure way to handle password hashing and verification in PHP applications.

Configuring Hashing Options

Setting Cost Factors

When using password hashing algorithms in PHP, you can adjust the cost factor to balance security and performance. The cost factor determines how much computation time is needed to create the hash.

Balancing security and performance:

A higher cost factor increases security but also increases the time and resources needed to create the hash. This can affect your application's performance, especially during high traffic. A lower cost factor is faster but less secure.

To find a good balance, test different cost factors on your server. Measure how long it takes to hash a password with various cost settings. Choose the highest cost that doesn't slow down your application too much.

Here's a simple way to test hashing time:

$timeTarget = 0.05; // 50 milliseconds 

$cost = 8;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test_password", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);

echo "Appropriate cost found: " . $cost;

This script increases the cost until the hashing time reaches about 50 milliseconds.

Recommended cost values for different scenarios:

  • For most web applications: A cost of 10 to 12 is often a good starting point.
  • For high-security systems: Consider a cost of 13 to 15, but be aware of the performance impact.
  • For low-resource systems: A cost of 8 to 10 might be necessary to maintain performance.

As computers get faster, you should increase the cost factor to maintain security. Review and update your cost settings regularly.

When using Argon2, you can also adjust memory and parallelism factors along with time cost. These settings depend on your server resources and security needs.

Tip: Regular Updates

Check your hashing cost factors yearly. As hardware improves, you may need to increase the cost to maintain the same level of security.

Example: Logging Hash Times

function logHashTime($cost) {
    $start = microtime(true);
    password_hash("test_password", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
    $time = $end - $start;

    error_log("Hash time for cost {$cost}: {$time} seconds");
}

// Log times for different costs
for ($cost = 8; $cost <= 15; $cost++) {
    logHashTime($cost);
}

This example shows how to log hash times for different cost values, which can help you track performance over time and make informed decisions about adjusting your cost factor.

Storing Hashed Passwords in Databases

When storing hashed passwords in databases, follow these best practices:

  1. Use a column for hashed passwords. Create a column in your users table for storing the hashed passwords. This column should be VARCHAR with enough length to store the hash output (60-255 characters, depending on the hashing algorithm).

  2. Store the full hash output. The output from password_hash() includes the algorithm, cost, and salt information. Store this entire string, as it's needed for password verification later.

  3. Never store plain-text passwords. Always hash passwords before storing them in the database.

  4. Use prepared statements. When inserting or updating password hashes in the database, use prepared statements to prevent SQL injection attacks.

  5. Limit access to the password column. Use database permissions to restrict access to the password column to only the necessary database users or roles.

To avoid storage issues:

  1. Don't shorten the hash. Some developers shorten the hash to save space. This breaks the hash and makes it unusable.

  2. Don't use reversible encryption. Hashing is one-way and cannot be reversed. Avoid using encryption methods that can be decrypted.

  3. Don't store the salt separately. Modern hashing functions like bcrypt include the salt in the hash output. There's no need to store it in a separate column.

  4. Don't use old hash functions. Avoid using outdated functions like MD5 or SHA-1, which are not secure for password hashing.

  5. Don't log or display hashed passwords. Treat hashed passwords as sensitive information and avoid including them in logs or displaying them in any user interface.

Tip: Regularly Update Hashing Algorithms

Stay informed about the latest security recommendations and update your hashing algorithms when needed. As computing power increases, older algorithms may become vulnerable. Implement a system to rehash passwords using newer, more secure algorithms when users log in or change their passwords.

Handling Password Updates and Migrations

When upgrading password hashing algorithms or migrating to a new system, it's important to handle the process carefully to maintain security and minimize user impact. Here are some strategies for managing password updates and migrations:

Strategies for upgrading hashing algorithms:

  • Gradual migration: Implement a system that can work with both old and new hashing algorithms. When users log in, check if their password is hashed with the old algorithm. If so, rehash it with the new algorithm and update the database.

  • Flag system: Add a flag in your database to indicate which hashing algorithm was used for each user's password. This allows you to support multiple algorithms during the transition period.

  • Forced password reset: For high-security systems, you might require all users to reset their passwords when upgrading to a new hashing algorithm. While this ensures immediate adoption, it can be inconvenient for users.

User password migration:

  • Transparent updates: When a user logs in successfully with the old algorithm, rehash their password with the new algorithm without requiring any action from the user.

  • Opportunistic migration: Update passwords to the new algorithm when users perform password-related actions, such as changing their password or requesting a reset.

  • Background processing: For large systems, implement a background process that gradually migrates passwords to the new algorithm during off-peak hours.

Here's a PHP example of how to implement a gradual migration:

function verifyAndUpdatePassword($password, $hash) {
    if (password_verify($password, $hash)) {
        // Password is correct
        if (password_needs_rehash($hash, PASSWORD_DEFAULT)) {
            // Hash was created with an old algorithm, update it
            $newHash = password_hash($password, PASSWORD_DEFAULT);
            // Update the hash in the database
            updatePasswordInDatabase($newHash);
        }
        return true;
    }
    return false;
}

This function checks if the password is correct and, if so, checks if it needs to be rehashed with the current default algorithm. If rehashing is needed, it updates the hash in the database.

Tip: Communication is Key

When implementing password changes, communicate clearly with your users. Explain why the changes are happening and how it benefits their security. This helps maintain trust and reduces support requests.

Example: Logging Migration Progress

function migratePasswords() {
    $users = getUsersWithOldHashAlgorithm();
    foreach ($users as $user) {
        $newHash = password_hash($user['password'], PASSWORD_DEFAULT);
        updateUserPasswordHash($user['id'], $newHash);
        logMigrationProgress($user['id']);
    }
}

function logMigrationProgress($userId) {
    $log = "User ID: " . $userId . " - Password hash migrated at " . date('Y-m-d H:i:s');
    file_put_contents('migration_log.txt', $log . PHP_EOL, FILE_APPEND);
}

This example shows how to log the progress of password migrations, which is useful for tracking the process and troubleshooting if needed.

By using these strategies, you can upgrade your password hashing methods while minimizing disruption to your users and maintaining security throughout the migration process.