Configure database-side password validation¶
The most applications read the password (hash) from a database to compare it with the user provided ones. An alternative is to validate the password inside the database by using a stored procedure. The application now cannot even read the password hash. Additionally, the 'last used' value of device passwords is automatically updated on each use.
Unfortunately, this technique has its downside:
- Some applications do need password (hash) access and do not work with database-side validation For example a RADIUS server performing MS-CHAPv2 authentication does need password access.
- The password must be transferred to the database server. If query logging is enabled, this may lead to the plaintext password being logged.
- It is highly database specific. Therefore, this guide will only cover some configurations.
Postgres¶
This guide requires the pgcrypto extension to be installed. The password hashing algorithm must be either set to bcrypt (recommended!) or md5_crypt.
Enable and validate the pgcrypto extension¶
Enable the extension first, if not already enabled:
Validate if the extension works by validating the crypt(password, salt or hash) works:
SELECT
crypt('valid', REPLACE('$2b$12$Se3ZAEkAhdiF8yOBSLWN/eKg5SVFGZdm2SweJiMwWkjs0nUPPlbqO', '$2b$', '$2a$')) = '$2a$12$Se3ZAEkAhdiF8yOBSLWN/eKg5SVFGZdm2SweJiMwWkjs0nUPPlbqO' as bcrypt_works,
crypt('valid', '$1$y6yBElVm$.QubZncgyfJEM7BOd.grj/') = '$1$y6yBElVm$.QubZncgyfJEM7BOd.grj/' as md5_crypt_works;
bcrypt_works and md5_crypt_works with a true value:
Add the stored procedure¶
The following SQL statements create three functions:
valid_password_no_audit(IN password TEXT, IN password_hash TEXT) RETURNS BOOLEAN: Check if a password matches the given hash.valid_password(IN password TEXT, IN password_hash TEXT, IN token_id uuid) RETURNS BOOLEAN: Check if a password matches the given hash matches. If it matches, this will audit this.valid_login(IN given_user TEXT, IN given_email TEXT, IN password TEXT) RETURNS TABLE(sub VARCHAR, username VARCHAR, email VARCHAR): Returns the subject claim, username and email address of a user only if the password matches a non expired token. If username lookup should not be based upon either username or password, set parameter to a blank value. The last used value will be updated if the password is correct.
CREATE OR REPLACE FUNCTION
valid_password_no_audit(IN password TEXT, IN password_hash TEXT)
RETURNS BOOLEAN
LANGUAGE SQL
BEGIN ATOMIC
RETURN crypt(password, REPLACE(password_hash, '$2b$', '$2a$')) = REPLACE(password_hash, '$2b$', '$2a$');
END;
CREATE OR REPLACE FUNCTION
valid_password(IN password TEXT, IN password_hash TEXT, IN token_id uuid)
RETURNS BOOLEAN
LANGUAGE plpgsql
AS $$
BEGIN
IF valid_password_no_audit(password, password_hash) THEN
INSERT INTO logs(date, "tokenId") VALUES (now(), token_id);
RETURN TRUE;
end if;
RETURN FALSE;
END;
$$;
CREATE OR REPLACE FUNCTION
valid_login(IN given_user TEXT, IN given_email TEXT, IN password TEXT)
RETURNS TABLE(sub VARCHAR, username VARCHAR, email VARCHAR) ROWS 1
LANGUAGE plpgsql
AS $$
BEGIN
RETURN QUERY
SELECT
users.sub, users.username, users.email
FROM
users
INNER JOIN
public.tokens t on users.sub = t.sub
WHERE
((given_user <> '' AND users.username = given_user) OR
(given_email <> '' AND users.email = given_email)) AND
(t.expires ISNULL OR t.expires < now()) AND
valid_password(password, t.token, t.id);
END;
$$;
The following statement shows an example validation function: