Skip to main content
Hashes a password using the Argon2id algorithm, suitable for secure storage.

Signature

sqlpage.hash_password(password TEXT) -> TEXT

Parameters

password
TEXT
required
The plaintext password to hash

Return Value

return
TEXT
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;

Hash Format

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:
  1. Extracts the salt and parameters from the stored hash
  2. Hashes the provided password with the same salt and parameters
  3. Compares the resulting hash with the stored hash
  4. 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;

Performance Considerations

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

Build docs developers (and LLMs) love