Hashes a password using the Argon2id algorithm, suitable for secure storage.
Signature
sqlpage.hash_password(password TEXT) -> TEXT
Parameters
The plaintext password to hash
Return Value
The hashed password in PHC string format, or NULL if input is NULL
Description
The hash_password() function securely hashes passwords using the Argon2id algorithm, which is the winner of the Password Hashing Competition and recommended by OWASP.
Key features:
- Unique salt - Every hash uses a different random salt
- CPU and memory intensive - Resistant to brute-force attacks
- PHC format - Standard format including algorithm, parameters, salt, and hash
- Rainbow-table resistant - Same password produces different hashes
When to Use
Use hash_password() when:
- Creating a new user account
- Resetting a user’s password
- Allowing users to change their password
Do NOT use hash_password() when:
- Verifying login credentials (use the authentication component instead)
- Hashing the same password repeatedly (each call generates a new hash)
Examples
Create User with Hashed Password
Register a new user with a secure password:
SELECT 'form' AS component,
'register.sql' as action;
SELECT 'username' AS name, 'text' as type;
SELECT 'password' AS name, 'password' AS type;
Handle the registration:
-- Hash the password and insert user
INSERT INTO users (username, password_hash, created_at)
VALUES (
:username,
sqlpage.hash_password(:password),
CURRENT_TIMESTAMP
);
SELECT 'redirect' as component, '/login.sql' as link;
Password Reset
Allow users to reset their password:
-- Verify reset token
SET user_id = (
SELECT user_id
FROM password_reset_tokens
WHERE token = :token
AND expires_at > CURRENT_TIMESTAMP
);
SELECT 'redirect' as component, '/error.sql' as link
WHERE $user_id IS NULL;
-- Update password
UPDATE users
SET password_hash = sqlpage.hash_password(:new_password),
updated_at = CURRENT_TIMESTAMP
WHERE id = $user_id;
-- Invalidate reset token
DELETE FROM password_reset_tokens WHERE user_id = $user_id;
SELECT 'redirect' as component, '/login.sql?msg=password_reset' as link;
Change Password
Let authenticated users change their password:
SET username = sqlpage.basic_auth_username();
-- Verify old password using authentication component
SELECT 'authentication' AS component,
(SELECT password_hash FROM users WHERE username = $username) AS password_hash,
sqlpage.basic_auth_password() AS password;
-- Update to new password
UPDATE users
SET password_hash = sqlpage.hash_password(:new_password)
WHERE username = $username;
SELECT 'alert' as component,
'Success' as title,
'Your password has been changed.' as description,
'green' as color;
Admin Creates User
Admin interface to create user accounts:
-- Check admin privileges
SET is_admin = (SELECT is_admin FROM users WHERE username = sqlpage.basic_auth_username());
SELECT 'redirect' as component, '/forbidden.sql' as link
WHERE NOT $is_admin;
-- Create new user with generated password
SET generated_password = sqlpage.random_string(16);
INSERT INTO users (username, password_hash, created_by)
VALUES (
:username,
sqlpage.hash_password($generated_password),
$is_admin
)
RETURNING
'alert' as component,
'User Created' as title,
'Username: ' || username || ', Password: ' || $generated_password as description,
'yellow' as color;
The function returns a hash in PHC string format:
$argon2id$v=19$m=19456,t=2,p=1$R3z5lbzT...$lK8dW+s2...
│ │ │ │ │
│ │ │ │ └─ Hash
│ │ │ └─────────── Salt
│ │ └─────────────────────── Parameters
│ └──────────────────────────────── Version
└─────────────────────────────────────── Algorithm
Parameters:
m=19456 - Memory cost (19 MB)
t=2 - Time cost (2 iterations)
p=1 - Parallelism (1 thread)
Security Properties
Unique Salts
Each call generates a different hash, even for the same password:
SELECT sqlpage.hash_password('password123');
-- $argon2id$v=19$m=19456,t=2,p=1$abc123...$def456...
SELECT sqlpage.hash_password('password123');
-- $argon2id$v=19$m=19456,t=2,p=1$xyz789...$uvw012...
-- Different hash!
This means:
- Rainbow tables are useless
- Two users with the same password have different hashes
- You can safely show hashes without revealing passwords
Brute-Force Resistance
Argon2 is designed to be computationally expensive:
-- Hashing is intentionally slow (~50-100ms per password)
SET start_time = datetime('now');
SET hash = sqlpage.hash_password('test');
SET end_time = datetime('now');
-- This slowness makes brute-force attacks impractical
Verifying Passwords
Use the authentication component to verify passwords:
-- During login
SELECT 'authentication' AS component,
(SELECT password_hash FROM users WHERE username = :username) AS password_hash,
:password AS password;
-- If authentication succeeds, user is logged in
SELECT 'redirect' as component, '/dashboard.sql' as link;
The authentication component:
- Extracts the salt and parameters from the stored hash
- Hashes the provided password with the same salt and parameters
- Compares the resulting hash with the stored hash
- Returns an error if they don’t match
Best Practices
1. Never Store Plaintext Passwords
-- WRONG - Security disaster!
INSERT INTO users (username, password)
VALUES (:username, :password);
-- CORRECT - Always hash
INSERT INTO users (username, password_hash)
VALUES (:username, sqlpage.hash_password(:password));
2. Hash Only When Creating/Changing Passwords
-- WRONG - Don't hash during login
WHERE password_hash = sqlpage.hash_password(:password) -- Won't work!
-- CORRECT - Use authentication component
SELECT 'authentication' AS component,
password_hash,
:password AS password
FROM users WHERE username = :username;
3. Combine with Strong Password Requirements
SET password = :password;
-- Enforce password policy
SELECT 'redirect' as component, '/weak-password.sql' as link
WHERE LENGTH($password) < 8
OR $password NOT GLOB '*[A-Z]*'
OR $password NOT GLOB '*[a-z]*'
OR $password NOT GLOB '*[0-9]*';
-- Hash if password meets requirements
INSERT INTO users (username, password_hash)
VALUES (:username, sqlpage.hash_password($password));
4. Add Rate Limiting
Protect password reset and registration endpoints:
SET ip = sqlpage.client_ip();
SET recent_resets = (
SELECT COUNT(*) FROM password_resets
WHERE ip_address = $ip
AND requested_at > datetime('now', '-1 hour')
);
SELECT 'redirect' as component, '/rate-limit.sql' as link
WHERE $recent_resets >= 3;
Password hashing is intentionally slow to prevent brute-force attacks:
- Typical time: 50-100ms per hash
- Impact: Minimal for legitimate use cases
- Not suitable for: Hashing in loops or bulk operations
-- AVOID - Hashing in a loop
SELECT
username,
sqlpage.hash_password(default_password) -- Slow!
FROM pending_users;
-- BETTER - Hash once, reuse if appropriate
SET default_hash = sqlpage.hash_password('ChangeMe123');
UPDATE pending_users SET password_hash = $default_hash;
See Also