Password
Email and password authentication with Argon2 hashing
Password Plugin
The Password plugin provides classic email + password authentication. Passwords are hashed with Argon2id, which is resistant to GPU and ASIC brute-force attacks.
Setup
Add the plugin to your model:
class User < ApplicationRecord
include Custos::Authenticatable
custos do
plugin :password
end
endRun the model generator to create the necessary migration:
rails generate custos:model User password
rails db:migrateThis adds a password_digest column to your users table.
Configuration Options
Pass options when enabling the plugin:
custos do
plugin :password,
min_length: 12,
max_length: 128,
require_uppercase: true,
require_digit: true,
require_special: true
end| Option | Type | Default | Description |
|---|---|---|---|
min_length | Integer | 8 | Minimum password length |
max_length | Integer | 128 | Maximum password length |
require_uppercase | Boolean | false | Require at least one uppercase letter |
require_digit | Boolean | false | Require at least one digit |
require_special | Boolean | false | Require at least one special character |
t_cost | Integer | Argon2 default | Argon2 time cost (iterations) |
m_cost | Integer | Argon2 default | Argon2 memory cost (2^m_cost KiB) |
p_cost | Integer | Argon2 default | Argon2 parallelism factor |
All validations use standard Active Model validations, so errors appear on record.errors as you'd expect.
Argon2 Tuning
The default Argon2 parameters work well for most applications. If you need to tune memory usage (for example, to limit DoS risk on memory-constrained servers), pass Argon2 cost parameters:
custos do
plugin :password,
t_cost: 2, # iterations (higher = slower)
m_cost: 16, # memory: 2^16 = 64 MiB (default)
p_cost: 1 # parallelism
endHigher m_cost values provide better security but use more memory per hash operation. On shared or low-memory hosts, consider reducing m_cost and increasing t_cost to compensate.
Instance Methods
password=(plain_password)
Sets the password. Hashes the plain-text password with Argon2 and stores the result in password_digest.
user = User.new(email: "alice@example.com")
user.password = "SecurePassword123!"
user.save!authenticate_password(plain_password)
Verifies a plain-text password against the stored digest. Returns true or false.
user.authenticate_password("SecurePassword123!")
# => true
user.authenticate_password("wrong")
# => falseIf the Lockout plugin is also enabled, authenticate_password automatically records failed attempts and resets the counter on success.
Class Methods
find_by_email_and_password(email:, password:)
Finds a user by email and verifies the password in one call. Returns the user record on success, nil on failure.
user = User.find_by_email_and_password(
email: "alice@example.com",
password: "SecurePassword123!"
)If the account is locked (Lockout plugin enabled), this method returns nil without attempting password verification.
Controller Example
class SessionsController < ApplicationController
def create
user = User.find_by_email_and_password(
email: params[:email],
password: params[:password]
)
if user
_session, token = Custos::SessionManager.create(user, request: request)
cookies.signed[:custos_session_token] = {
value: token,
httponly: true,
secure: Rails.env.production?
}
redirect_to dashboard_path
else
flash[:alert] = "Invalid email or password"
render :new, status: :unprocessable_entity
end
end
endDatabase Schema
The Password plugin adds one column to your model's table:
| Column | Type | Notes |
|---|---|---|
password_digest | string | Argon2id hash |
Security Notes
- Passwords are hashed with Argon2id, the winner of the Password Hashing Competition
- The
@passwordinstance variable is cleared automatically after save -- plain-text passwords are not kept in memory - Password verification includes timing-safe comparison (built into Argon2)
find_by_email_and_passwordperforms a dummy Argon2 verify when no user is found, preventing timing-based user enumeration- Corrupted password digests are handled gracefully (returns
false, not an error) - When combined with Lockout, accounts are protected against brute-force attacks via the
after_authenticationevent hook