A small web service to verify user emails and passwords against stored Wordpress password hashes.
The server responds with a verified/unverified decision whenever an email and
raw password are POSTed to the /verify endpoint. The server loads a CSV file
holding emails and Wordpress-hashed passwords into memory on startup.
The server can only verify requests against hashes from newer Wordpress-flavored
bcrypt - the default in Wordpress 6.8 - and earlier Wordpress versions using
phpass. These hashes have a $P$ or $wp prefix. The server will fail on
startup if the CSV file has hashes with unknown prefixes.
The CSV file must be UTF-8 encoded with a header and columns giving user_email
and user_pass. No duplicate user emails are allowed and will cause the server to fail on startup.
The server will also respond with user profiles to GET requests to /users with an email parameter.
Warning
This application does not have features to securely run alone in a production environment.
Generate a CSV file with user email addresses and hashed passwords in an example directory.
EXAMPLE_DIR="./exampledata"
DB_FILE="wp_db.csv"
DB_PATH="${EXAMPLE_DIR}/${DB_FILE}"
mkdir $EXAMPLE_DIR
echo 'user_email,user_pass' > $DB_PATH
echo 'johndoe@example.com,$wp$2y$10$gN3SQdbNc/cVlK7DylUiVumiuujud7lR0h5J4M2ZsNRMYOFbED16q' >> $DB_PATH
echo 'janedoe@example.com,$P$BsSozX7pxy0bajB//ff34WOg4vN9OI/' >> $DB_PATHNow start the server. Here, we're using the container.
docker run --rm -p 127.0.0.1:8000:8000 \
-v "${EXAMPLE_DIR}:/data:ro" \
-e CSV_PATH="/data/${DB_FILE}" \
-e HOST="0.0.0.0" \
-e PORT="8000" \
ghcr.io/brews/wenceslas:latestTip
This example is using the latest tag for demonstration. In a production environment best practice is to use a tag or hash for a specific released version, such as 1.0.0. Tagged images are available here.
Send requests to the server like
curl -X POST "http://localhost:8000/verify" \
-H 'Content-Type: application/json' \
--data-raw '{"email": "johndoe@example.com", "password": "Test123Now!"}'and the server responds with
{"verified":true}A request with bad email or password like
curl -X POST "http://localhost:8000/verify" \
-H 'Content-Type: application/json' \
--data-raw '{"email": "janedoe@example.com", "password": "Test123Now!"}'gets the response
{"verified":false}Poorly formatted request bodies will get a (hopefully) descriptive error message and a 422 response status code.
You can set an optional API key when the server starts. The server will only allow POST requests with this key in the header of each request. For example, we can generate a length key like
APIKEY=$(openssl rand -base64 40)And pass the key to the server on startup like
docker run --rm -p 127.0.0.1:8000:8000 \
-v "${EXAMPLE_DIR}:/data:ro" \
-e CSV_PATH="/data/${DB_FILE}" \
-e HOST="0.0.0.0" \
-e PORT="8000" \
-e APIKEY="${APIKEY}" \
ghcr.io/brews/wenceslas:latestAnd now requests need to include the key in the header of their requests like
curl -X POST "http://localhost:8000/verify" \
-H "x-apikey: ${APIKEY}" \
-H 'Content-Type: application/json' \
--data-raw '{"email": "johndoe@example.com", "password": "Test123Now!"}'or the server will reply with "401 Unauthorized" response status.
Again, it's worth noting that using the API key feature does not secure network communication. Simply using the API key feature is not adequate security for running this in a production environment.
You can also use the /user endpoint to get user profiles. For example
curl -X GET "http://localhost:8000/users?email=johndoe%40example.com" \
-H "x-apikey: ${APIKEY}" \
-H 'Content-Type: application/json'is a request for the profile to johndoe@example.com and gets the response
[{"user_email":"johndoe@example.com","display_name":null,"first_name":null,"last_name":null,"nickname":null,"organization":null}]
The response contains nulls because these fields are optional, and not columns in the input CSV.
The response is a 404 if no profile is found for the requested email.
Similar information can be found with
curl -X POST "http://localhost:8000/email-query" \
-H "x-apikey: ${APIKEY}" \
-H 'Content-Type: application/json'
--data-raw '{"email": "johndoe@example.com"}'which gets the response
{"users": [{"user_email":"johndoe@example.com","display_name":null,"first_name":null,"last_name":null,"nickname":null,"organization":null}]}
The "users" array is emtpy if there is no match. For example,
curl -X POST "http://localhost:8000/email-query" \
-H "x-apikey: ${APIKEY}" \
-H 'Content-Type: application/json'
--data-raw '{"email": "no@match.com"}'gets the response
{"users": []}
The benefit of POSTing to the /email-query endpoint over GETing the /users endpoint is that user emails in the POST request are not part of the URL, thus, less likely to be logged by intermediate systems, respecting user privacy.
wenceslas is typically run from prebuilt container images.
To build this from source you will need the Git Version Control System and the Rust toolchain.
First clone the repository.
Compile and run from source
# Use --locked for deterministic builds.
cargo run --locked
# Use --release for optimized builds without debugging symbolsWith docker istalled and configured for your system, run
docker build -t wenceslas:dev .to build a container image tagged wenceslas:dev.
Configurations are set through environment variables.
- CSV_PATH: Required. Path to CSV file with user email and Wordpress password hashes. Must have columns and header
user_emailanduser_pass. - HOST: Required. IPv4 or IPv6 address to listen for requests. E.g. "127.0.0.1" or "::1".
- PORT: Required. Port over which to listen for requests. E.g. "8000".
- APIKEY: Key to check for in request headers under the "x-apikey" field. No auth is used if this is not set.
- RUST_LOG: Logging level. E.g. "TRACE".
wenceslas releases use semantic versioning. Best effort is made to note breaking changes to the application and its web API as documented. This does not include breaking changes to the application's Rust API.
wenceslas is open-source software made available under the terms of either the MIT License or the Apache License 2.0, at your option.