I'm looking for guidance on implementing user authentication in a Flask application that integrates with WordPress 6.8, specifically focusing on how to handle the new password hashing methods used by WordPress.
Cause after updating WordPress to 6.8, it now has different hash than before.
$P$
$wp$2y$10$
My previous code was working fine before updating WordPress to 6.8 :
# ...
from passlib.hash import phpass
class Authenticator:
def __init__(self, flask_app):
self.flask_app = flask_app
def authenticate(self, email, password):
user_password = self.get_user_password(email)
# user password from DB looks like: $P$.....
# password looks like (Plain text): "something"
if not phpass.verify(password, user_password):
return False, Response.wrong_credentials(self.flask_app.site_url)
return True, None
I tried using flask_bcrypt
but I got an error: Invalid salt
# ...
from flask_bcrypt import Bcrypt
class Authenticator:
def __init__(self, flask_app):
self.flask_app = flask_app
self.bcrypt = Bcrypt(flask_app)
def authenticate(self, email, password):
user_password = self.get_user_password(email)
# user password in DB looks like: $WP$2y$10$...
# password looks like (Plain text): "something"
try:
if not self.bcrypt.check_password_hash(user_password, password):
return False, Response.wrong_credentials(self.flask_app.site_url)
except Exception as e:
print(e) # it throw error: Invalid Salt
return True, None
CAVEAT: I have not tested this.
The source code for WordPress version 6.8.1's wp_hash_password
in WordPress/wp-includes/pluggable.php
contains these lines:
// ...
// Use SHA-384 to retain entropy from a password that's longer than 72 bytes, and a `wp-sha384` key for domain separation.
$password_to_hash = base64_encode( hash_hmac( 'sha384', trim( $password ), 'wp-sha384', true ) );
// Add a prefix to facilitate distinguishing vanilla bcrypt hashes.
return '$wp' . password_hash( $password_to_hash, $algorithm, $options );
This shows that, before bcrypt is applied, a pre-hash is performed using HMAC-SHA384 with a key of wp-sha384
. The pre-hash eliminates bcrypt's limit of no more than 72 bytes for the password. This somewhat unique pre-hash helps mitigate a potential security issue of pre-hashing generally called password shucking, as it is unlikely this particular hash was ever used in any other system previously.
The equivalent python code could look something like:
import hmac
import hashlib
from typing import Union
from passlib.hash import bcrypt
def wp_pre_hash(password: bytes) -> bytes:
hmac_sha384 = hmac.new('wp-sha384'.encode('utf-8'), digestmod=hashlib.sha384)
hmac_sha384.update(password)
return hmac_sha384.digest()
def wp_password_hash(password: Union[bytes, str]) -> str:
if isinstance(password, str):
password = password.encode('utf-8')
pre_hash = wp_pre_hash(password)
final_hash = bcrypt.using(rounds=10, ident='2y').hash(pre_hash)
return '$wp' + final_hash
def wp_password_verify(password: Union[bytes, str], hashed_password: bytes) -> bool:
if hashed_password[:3] != '$wp':
raise ValueError('not WordPress >= 6.8 password hash')
hashed_password = hashed_password[3:]
if isinstance(password, str):
password = password.encode('utf-8')
pre_hash = wp_pre_hash(password)
verified = bcrypt.verify(pre_hash, hashed_password)
return verified