- FEATURES =
{}
- VERSION =
'1.3.0'.freeze
- Jwt =
Feature.define(:jwt) do
auth_value_method :json_non_post_error_message, 'non-POST method used in JSON API'
auth_value_method :json_response_error_status, 400
auth_value_method :json_response_error_key, "error"
auth_value_method :json_response_field_error_key, "field-error"
auth_value_method :json_response_success_key, nil
auth_value_method :jwt_algorithm, "HS256"
auth_value_method :non_json_request_error_message, 'Only JSON format requests are allowed'
auth_value_method :only_json?, true
auth_value_methods :jwt_secret
auth_methods(
:json_request?,
:jwt_token,
:set_jwt_token
)
def session
return super unless json_request?
return @session if defined?(@session)
@session = if token = jwt_token
s = {}
payload, = JWT.decode(token, jwt_secret, true, :algorithm=>jwt_algorithm)
payload.each do |k,v|
s[k.to_sym] = v
end
s
else
{}
end
end
def clear_session
super
set_jwt if json_request?
end
def set_field_error(field, message)
return super unless json_request?
json_response[json_response_field_error_key] = [field, message]
end
def set_error_flash(message)
return super unless json_request?
json_response[json_response_error_key] = message
end
def set_redirect_error_flash(message)
return super unless json_request?
json_response[json_response_error_key] = message
end
def set_notice_flash(message)
return super unless json_request?
json_response[json_response_success_key] = message if include_success_messages?
end
def set_notice_now_flash(message)
return super unless json_request?
json_response[json_response_success_key] = message if include_success_messages?
end
def jwt_token
if v = request.env['HTTP_AUTHORIZATION']
v.sub(/\ABearer:?\s+/, '')
end
end
def set_jwt_token(token)
response.['Authorization'] = token
end
private
def before_rodauth
if only_json? && !json_request?
response.status = json_response_error_status
response.write non_json_request_error_message
request.halt
end
if json_request? && !request.post?
response.status = 405
response.['Allow'] = 'POST'
json_response[json_response_error_key] = json_non_post_error_message
return_json_response
end
super
end
def before_view_recovery_codes
super if defined?(super)
if json_request?
json_response[:codes] = recovery_codes
json_response[json_response_success_key] ||= "" if include_success_messages?
end
end
def jwt_secret
raise ArgumentError, "jwt_secret not set"
end
def redirect(_)
return super unless json_request?
return_json_response
end
def include_success_messages?
!json_response_success_key.nil?
end
def set_session_value(key, value)
super
set_jwt if json_request?
value
end
def json_response
@json_response ||= {}
end
def _view(meth, page)
return super unless json_request?
return super if meth == :render
return_json_response
end
def return_json_response
response.status ||= json_response_error_status if json_response[json_response_error_key]
set_jwt
response.write(request.send(:convert_to_json, json_response))
request.halt
end
def set_jwt
set_jwt_token(JWT.encode(session, jwt_secret, jwt_algorithm))
end
def json_request?
return @json_request if defined?(@json_request)
@json_request = request.content_type =~ /application\/json/
end
end
- Otp =
Feature.define(:otp) do
depends :two_factor_base
additional_form_tags 'otp_disable'
additional_form_tags 'otp_auth'
additional_form_tags 'otp_setup'
after 'otp_authentication_failure'
after 'otp_disable'
after 'otp_setup'
before 'otp_authentication'
before 'otp_setup'
before 'otp_disable'
before 'otp_authentication_route'
before 'otp_setup_route'
before 'otp_disable_route'
button 'Authenticate via 2nd Factor', 'otp_auth'
button 'Disable Two Factor Authentication', 'otp_disable'
button 'Setup Two Factor Authentication', 'otp_setup'
error_flash "Error disabling up two factor authentication", 'otp_disable'
error_flash "Error logging in via two factor authentication", 'otp_auth'
error_flash "Error setting up two factor authentication", 'otp_setup'
error_flash "You have already setup two factor authentication", :otp_already_setup
notice_flash "Two factor authentication has been disabled", 'otp_disable'
notice_flash "Two factor authentication is now setup", 'otp_setup'
redirect :otp_disable
redirect :otp_already_setup
redirect :otp_setup
view 'otp-disable', 'Disable Two Factor Authentication', 'otp_disable'
view 'otp-auth', 'Enter Authentication Code', 'otp_auth'
view 'otp-setup', 'Setup Two Factor Authentication', 'otp_setup'
auth_value_method :otp_auth_failures_limit, 5
auth_value_method :otp_auth_label, 'Authentication Code'
auth_value_method :otp_auth_param, 'otp'
auth_value_method :otp_class, ROTP::TOTP
auth_value_method :otp_digits, nil
auth_value_method :otp_drift, nil
auth_value_method :otp_interval, nil
auth_value_method :otp_invalid_auth_code_message, "Invalid authentication code"
auth_value_method :otp_invalid_secret_message, "invalid secret"
auth_value_method :otp_keys_column, :key
auth_value_method :otp_keys_id_column, :id
auth_value_method :otp_keys_failures_column, :num_failures
auth_value_method :otp_keys_table, :account_otp_keys
auth_value_method :otp_keys_last_use_column, :last_use
auth_value_method :otp_setup_param, 'otp_secret'
auth_cached_method :otp_key
auth_cached_method :otp
private :otp
auth_value_methods(
:otp_auth_form_footer,
:otp_issuer,
:otp_lockout_error_flash,
:otp_lockout_redirect
)
auth_methods(
:otp,
:otp_exists?,
:otp_key,
:otp_locked_out?,
:otp_new_secret,
:otp_provisioning_name,
:otp_provisioning_uri,
:otp_qr_code,
:otp_record_authentication_failure,
:otp_remove,
:otp_remove_auth_failures,
:otp_update_last_use,
:otp_valid_code?,
:otp_valid_key?
)
auth_private_methods(
:otp_add_key,
:otp_tmp_key
)
route(:otp_auth) do |r|
require_login
require_account_session
require_two_factor_not_authenticated
require_otp_setup
if otp_locked_out?
set_redirect_error_flash otp_lockout_error_flash
redirect otp_lockout_redirect
end
before_otp_authentication_route
r.get do
otp_auth_view
end
r.post do
if otp_valid_code?(param(otp_auth_param)) && otp_update_last_use
before_otp_authentication
two_factor_authenticate(:totp)
end
otp_record_authentication_failure
after_otp_authentication_failure
set_field_error(otp_auth_param, otp_invalid_auth_code_message)
set_error_flash otp_auth_error_flash
otp_auth_view
end
end
route(:otp_setup) do |r|
require_account
if otp_exists?
set_redirect_error_flash otp_already_setup_error_flash
redirect otp_already_setup_redirect
end
before_otp_setup_route
r.get do
otp_tmp_key(otp_new_secret)
otp_setup_view
end
r.post do
secret = param(otp_setup_param)
catch_error do
unless otp_valid_key?(secret)
throw_error(otp_setup_param, otp_invalid_secret_message)
end
otp_tmp_key(secret)
unless two_factor_password_match?(param(password_param))
throw_error(password_param, invalid_password_message)
end
unless otp_valid_code?(param(otp_auth_param))
throw_error(otp_auth_param, otp_invalid_auth_code_message)
end
transaction do
before_otp_setup
otp_add_key
two_factor_update_session(:totp)
after_otp_setup
end
set_notice_flash otp_setup_notice_flash
redirect otp_setup_redirect
end
set_error_flash otp_setup_error_flash
otp_setup_view
end
end
route(:otp_disable) do |r|
require_account
require_otp_setup
before_otp_disable_route
r.get do
otp_disable_view
end
r.post do
if two_factor_password_match?(param(password_param))
transaction do
before_otp_disable
otp_remove
two_factor_remove_session
after_otp_disable
end
set_notice_flash otp_disable_notice_flash
redirect otp_disable_redirect
end
set_field_error(password_param, invalid_password_message)
set_error_flash otp_disable_error_flash
otp_disable_view
end
end
def two_factor_authentication_setup?
super || otp_exists?
end
def two_factor_need_setup_redirect
"#{prefix}/#{otp_setup_route}"
end
def two_factor_auth_required_redirect
"#{prefix}/#{otp_auth_route}"
end
def two_factor_remove
super
otp_remove
end
def two_factor_remove_auth_failures
super
otp_remove_auth_failures
end
def
super if defined?(super)
end
def otp_lockout_redirect
return super if defined?(super)
default_redirect
end
def otp_lockout_error_flash
"Authentication code use locked out due to numerous failures.#{super if defined?(super)}"
end
def require_otp_setup
unless otp_exists?
set_redirect_error_flash two_factor_not_setup_error_flash
redirect two_factor_need_setup_redirect
end
end
def otp_exists?
!otp_key.nil?
end
def otp_valid_code?(ot_pass)
return false unless otp_exists?
ot_pass = ot_pass.gsub(/\s+/, '')
if drift = otp_drift
otp.verify_with_drift(ot_pass, drift)
else
otp.verify(ot_pass)
end
end
def otp_remove
otp_key_ds.delete
super if defined?(super)
end
def otp_add_key
_otp_add_key(otp_key)
super if defined?(super)
end
def otp_update_last_use
otp_key_ds.
where(Sequel.date_add(otp_keys_last_use_column, :seconds=>(otp_interval||30)) < Sequel::CURRENT_TIMESTAMP).
update(otp_keys_last_use_column=>Sequel::CURRENT_TIMESTAMP) == 1
end
def otp_record_authentication_failure
otp_key_ds.update(otp_keys_failures_column=>Sequel.identifier(otp_keys_failures_column) + 1)
end
def otp_remove_auth_failures
otp_key_ds.update(otp_keys_failures_column=>0)
end
def otp_locked_out?
otp_key_ds.get(otp_keys_failures_column) >= otp_auth_failures_limit
end
def otp_provisioning_uri
otp.provisioning_uri(otp_provisioning_name)
end
def otp_issuer
request.host
end
def otp_provisioning_name
account[login_column]
end
def otp_qr_code
RQRCode::QRCode.new(otp_provisioning_uri).as_svg(:module_size=>8)
end
private
def clear_cached_otp
remove_instance_variable(:@otp) if defined?(@otp)
end
def otp_tmp_key(secret)
_otp_tmp_key(secret)
clear_cached_otp
end
def otp_valid_key?(secret)
secret =~ /\A[a-z2-7]{16}\z/
end
def otp_new_secret
ROTP::Base32.random_base32
end
def _otp_tmp_key(secret)
@otp_key = secret
end
def _otp_add_key(secret)
otp_key_ds.insert(otp_keys_id_column=>session_value, otp_keys_column=>secret)
end
def _otp_key
otp_key_ds.get(otp_keys_column)
end
def _otp
otp_class.new(otp_key, :issuer=>otp_issuer, :digits=>otp_digits, :interval=>otp_interval)
end
def otp_key_ds
db[otp_keys_table].where(otp_keys_id_column=>session_value)
end
def use_date_arithmetic?
true
end
end
- Base =
Feature.define(:base) do
before 'rodauth'
error_flash "Please login to continue", 'require_login'
auth_value_method :account_id_column, :id
auth_value_method :account_open_status_value, 2
auth_value_method :account_password_hash_column, nil
auth_value_method :account_select, nil
auth_value_method :account_status_column, :status_id
auth_value_method :account_unverified_status_value, 1
auth_value_method :accounts_table, :accounts
auth_value_method :default_redirect, '/'
auth_value_method :invalid_password_message, "invalid password"
auth_value_method :login_column, :email
auth_value_method :password_hash_id_column, :id
auth_value_method :password_hash_column, :password_hash
auth_value_method :password_hash_table, :account_password_hashes
auth_value_method :no_matching_login_message, "no matching login"
auth_value_method :login_param, 'login'
auth_value_method :login_label, 'Login'
auth_value_method :password_label, 'Password'
auth_value_method :password_param, 'password'
auth_value_method :modifications_require_password?, true
auth_value_method :session_key, :account_id
auth_value_method :prefix, ''
auth_value_method :require_bcrypt?, true
auth_value_method :skip_status_checks?, true
auth_value_method :title_instance_variable, nil
auth_value_method :unverified_account_message, "unverified account, please verify account before logging in"
redirect(:require_login){"#{prefix}/login"}
auth_value_methods(
:db,
:require_login_redirect,
:set_deadline_values?,
:use_date_arithmetic?,
:use_database_authentication_functions?
)
auth_methods(
:account_id,
:account_session_value,
:already_logged_in,
:authenticated?,
:clear_session,
:csrf_tag,
:function_name,
:logged_in?,
:login_required,
:open_account?,
:password_match?,
:random_key,
:redirect,
:session_value,
:set_error_flash,
:set_notice_flash,
:set_notice_now_flash,
:set_redirect_error_flash,
:set_title,
:unverified_account_message,
:update_session
)
auth_private_methods(
:account_from_login,
:account_from_session
)
configuration_module_eval do
def auth_class_eval(&block)
auth.class_eval(&block)
end
def account_model(model)
warn "account_model is deprecated, use db and accounts_table settings"
db model.db
accounts_table model.table_name
account_select model.dataset.opts[:select]
end
end
attr_reader :scope
attr_reader :account
def initialize(scope)
@scope = scope
end
def features
self.class.features
end
def request
scope.request
end
def response
scope.response
end
def session
scope.session
end
def flash
scope.flash
end
def route!
if meth = self.class.route_hash[request.remaining_path]
send(meth)
end
nil
end
def set_field_error(field, error)
(@field_errors ||= {})[field] = error
end
def field_error(field)
return nil unless @field_errors
@field_errors[field]
end
def account_id
account[account_id_column]
end
alias account_session_value account_id
def session_value
session[session_key]
end
alias logged_in? session_value
def account_from_login(login)
@account = _account_from_login(login)
end
def open_account?
skip_status_checks? || account[account_status_column] == account_open_status_value
end
def db
Sequel::DATABASES.first
end
def account_password_hash_column
nil
end
def check_already_logged_in
already_logged_in if logged_in?
end
def already_logged_in
nil
end
def clear_session
session.clear
end
def login_required
set_redirect_error_flash require_login_error_flash
redirect require_login_redirect
end
def set_title(title)
if title_instance_variable
scope.instance_variable_set(title_instance_variable, title)
end
end
def set_error_flash(message)
flash.now[:error] = message
end
def set_redirect_error_flash(message)
flash[:error] = message
end
def set_notice_flash(message)
flash[:notice] = message
end
def set_notice_now_flash(message)
flash.now[:notice] = message
end
def require_login
login_required unless logged_in?
end
def authenticated?
logged_in?
end
def require_authentication
require_login
end
def account_initial_status_value
account_open_status_value
end
def account_from_session
@account = _account_from_session
end
def csrf_tag
scope.csrf_tag if scope.respond_to?(:csrf_tag)
end
def button(value, opts={})
opts = {:locals=>{:value=>value, :opts=>opts}}
opts[:path] = template_path('button')
scope.render(opts)
end
def view(page, title)
set_title(title)
_view(:view, page)
end
def render(page)
_view(:render, page)
end
def post_configure
require 'bcrypt' if require_bcrypt?
db.extension :date_arithmetic if use_date_arithmetic?
route_hash= {}
self.class.routes.each do |meth|
route_hash["/#{send("#{meth.to_s.sub(/\Ahandle_/, '')}_route")}"] = meth
end
self.class.route_hash = route_hash.freeze
end
def password_match?(password)
if account_password_hash_column
BCrypt::Password.new(account[account_password_hash_column]) == password
elsif use_database_authentication_functions?
id = account_id
if salt = db.get(Sequel.function(function_name(:rodauth_get_salt), id))
hash = BCrypt::Engine.hash_secret(password, salt)
db.get(Sequel.function(function_name(:rodauth_valid_password_hash), id, hash))
end
else
if hash = password_hash_ds.get(password_hash_column)
BCrypt::Password.new(hash) == password
end
end
end
private
def update_session
clear_session
session[session_key] = account_session_value
end
def param(key)
param_or_nil(key).to_s
end
def param_or_nil(key)
value = request.params[key]
value.to_s unless value.nil?
end
def redirect(path)
request.redirect(path)
end
def transaction(opts={}, &block)
db.transaction(opts, &block)
end
if RUBY_VERSION >= '1.9'
def random_key
SecureRandom.urlsafe_base64(32)
end
else
def random_key
SecureRandom.hex(32)
end
end
def timing_safe_eql?(provided, actual)
provided = provided.to_s
Rack::Utils.secure_compare(provided.ljust(actual.length), actual) && provided.length == actual.length
end
def require_account
require_authentication
require_account_session
end
def require_account_session
unless account_from_session
clear_session
login_required
end
end
def catch_error(&block)
catch(:rodauth_error, &block)
end
def throw_error(field, error)
set_field_error(field, error)
throw :rodauth_error
end
def use_date_arithmetic?
set_deadline_values?
end
def set_deadline_values?
db.database_type == :mysql
end
def use_database_authentication_functions?
case db.database_type
when :postgres, :mysql, :mssql
true
else
false
end
end
def function_name(name)
if db.database_type == :mssql
"dbo.#{name}"
else
name
end
end
def _account_from_login(login)
ds = db[accounts_table].where(login_column=>login)
ds = ds.select(*account_select) if account_select
ds = ds.where(account_status_column=>[account_unverified_status_value, account_open_status_value]) unless skip_status_checks?
ds.first
end
def _account_from_session
ds = account_ds(session_value)
ds = ds.where(account_session_status_filter) unless skip_status_checks?
ds.first
end
def account_session_status_filter
{account_status_column=>account_open_status_value}
end
def template_path(page)
File.join(File.dirname(__FILE__), '../../../templates', "#{page}.str")
end
def account_ds(id=account_id)
raise ArgumentError, "invalid account id passed to account_ds" unless id
ds = db[accounts_table].where(account_id_column=>id)
ds = ds.select(*account_select) if account_select
ds
end
def password_hash_ds
db[password_hash_table].where(password_hash_id_column=>account_id)
end
def convert_timestamp(timestamp)
timestamp = db.to_application_timestamp(timestamp) if timestamp.is_a?(String)
timestamp
end
def retry_on_uniqueness_violation(&block)
if raises_uniqueness_violation?(&block)
yield
end
end
def raises_uniqueness_violation?(&block)
transaction(:savepoint=>:only, &block)
false
rescue unique_constraint_violation_class => e
e
end
def unique_constraint_violation_class
if db.adapter_scheme == :jdbc && db.database_type == :sqlite
Sequel::ConstraintViolation
else
Sequel::UniqueConstraintViolation
end
end
alias raised_uniqueness_violation raises_uniqueness_violation?
alias ignore_uniqueness_violation raises_uniqueness_violation?
def set_deadline_value(hash, column, interval)
if set_deadline_values?
hash[column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, interval)
end
end
def set_session_value(key, value)
session[key] = value
end
def update_hash_ds(hash, ds, values)
num = ds.update(values)
if num == 1
values.each do |k, v|
account[k] = v == Sequel::CURRENT_TIMESTAMP ? Time.now : v
end
end
num
end
def update_account(values, ds=account_ds)
update_hash_ds(account, ds, values)
end
def _view(meth, page)
auth = self
auth_template_path = template_path(page)
scope.instance_exec do
template_opts = find_template(parse_template_opts(page, :locals=>{:rodauth=>auth}))
unless File.file?(template_path(template_opts))
template_opts[:path] = auth_template_path
end
send(meth, template_opts)
end
end
end
- Login =
Feature.define(:login) do
notice_flash "You have been logged in"
error_flash "There was an error logging in"
view 'login', 'Login'
after
after 'login_failure'
before
before 'login_attempt'
additional_form_tags
button 'Login'
redirect
auth_value_method :login_form_footer, ''
route do |r|
check_already_logged_in
before_login_route
r.get do
login_view
end
r.post do
clear_session
catch_error do
unless account_from_login(param(login_param))
throw_error(login_param, no_matching_login_message)
end
before_login_attempt
unless open_account?
throw_error(login_param, unverified_account_message)
end
unless password_match?(param(password_param))
after_login_failure
throw_error(password_param, invalid_password_message)
end
transaction do
before_login
update_session
after_login
end
set_notice_flash login_notice_flash
redirect login_redirect
end
set_error_flash login_error_flash
login_view
end
end
attr_reader :login_form_header
end
- Logout =
Feature.define(:logout) do
notice_flash "You have been logged out"
view 'logout', 'Logout'
additional_form_tags
before
after
button 'Logout'
redirect{require_login_redirect}
auth_methods :logout
route do |r|
before_logout_route
r.get do
logout_view
end
r.post do
transaction do
before_logout
logout
after_logout
end
set_notice_flash logout_notice_flash
redirect logout_redirect
end
end
def logout
clear_session
end
end
- Lockout =
Feature.define(:lockout) do
depends :login, :email_base
view 'unlock-account-request', 'Request Account Unlock', 'unlock_account_request'
view 'unlock-account', 'Unlock Account', 'unlock_account'
before 'unlock_account'
before 'unlock_account_request'
after 'unlock_account'
after 'unlock_account_request'
additional_form_tags 'unlock_account'
additional_form_tags 'unlock_account_request'
button 'Unlock Account', 'unlock_account'
button 'Request Account Unlock', 'unlock_account_request'
error_flash "There was an error unlocking your account", 'unlock_account'
error_flash "This account is currently locked out and cannot be logged in to.", "login_lockout"
notice_flash "Your account has been unlocked", 'unlock_account'
notice_flash "An email has been sent to you with a link to unlock your account", 'unlock_account_request'
redirect :unlock_account
redirect :unlock_account_request
auth_value_method :unlock_account_autologin?, true
auth_value_method :max_invalid_logins, 100
auth_value_method :account_login_failures_table, :account_login_failures
auth_value_method :account_login_failures_id_column, :id
auth_value_method :account_login_failures_number_column, :number
auth_value_method :account_lockouts_table, :account_lockouts
auth_value_method :account_lockouts_id_column, :id
auth_value_method :account_lockouts_key_column, :key
auth_value_method :account_lockouts_deadline_column, :deadline
auth_value_method :account_lockouts_deadline_interval, {:days=>1}
auth_value_method :no_matching_unlock_account_key_message, 'No matching unlock account key'
auth_value_method :unlock_account_email_subject, 'Unlock Account'
auth_value_method :unlock_account_key_param, 'key'
auth_value_method :unlock_account_requires_password?, false
auth_value_methods(
:unlock_account_redirect,
:unlock_account_request_redirect
)
auth_methods(
:clear_invalid_login_attempts,
:create_unlock_account_email,
:generate_unlock_account_key,
:get_unlock_account_key,
:invalid_login_attempted,
:locked_out?,
:send_unlock_account_email,
:unlock_account_email_body,
:unlock_account_email_link,
:unlock_account,
:unlock_account_key
)
auth_private_methods :account_from_unlock_key
route(:unlock_account_request) do |r|
check_already_logged_in
before_unlock_account_request_route
r.post do
if account_from_login(param(login_param)) && get_unlock_account_key
transaction do
before_unlock_account_request
send_unlock_account_email
after_unlock_account_request
end
set_notice_flash unlock_account_request_notice_flash
else
set_redirect_error_flash no_matching_login_message
end
redirect unlock_account_request_redirect
end
end
route(:unlock_account) do |r|
check_already_logged_in
before_unlock_account_route
r.get do
if account_from_unlock_key(param(unlock_account_key_param))
unlock_account_view
else
set_redirect_error_flash no_matching_unlock_account_key_message
redirect require_login_redirect
end
end
r.post do
key = param(unlock_account_key_param)
unless account_from_unlock_key(key)
set_redirect_error_flash no_matching_unlock_account_key_message
redirect unlock_account_request_redirect
end
if !unlock_account_requires_password? || password_match?(param(password_param))
transaction do
before_unlock_account
unlock_account
after_unlock_account
if unlock_account_autologin?
update_session
end
end
set_notice_flash unlock_account_notice_flash
redirect unlock_account_redirect
else
set_field_error(password_param, invalid_password_message)
set_error_flash unlock_account_error_flash
unlock_account_view
end
end
end
def locked_out?
if t = convert_timestamp(account_lockouts_ds.get(account_lockouts_deadline_column))
if Time.now < t
true
else
unlock_account
false
end
else
false
end
end
def unlock_account
transaction do
remove_lockout_metadata
end
end
def clear_invalid_login_attempts
unlock_account
end
def invalid_login_attempted
ds = account_login_failures_ds.
where(account_login_failures_id_column=>account_id)
number = if db.database_type == :postgres
ds.returning(account_login_failures_number_column).
with_sql(:update_sql, account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1).
single_value
else
if ds.update(account_login_failures_number_column=>Sequel.expr(account_login_failures_number_column)+1) > 0
ds.get(account_login_failures_number_column)
end
end
unless number
ignore_uniqueness_violation{account_login_failures_ds.insert(account_login_failures_id_column=>account_id)}
number = 1
end
if number >= max_invalid_logins
@unlock_account_key_value = generate_unlock_account_key
hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>unlock_account_key_value}
set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
if e = raised_uniqueness_violation{account_lockouts_ds.insert(hash)}
raise e unless @unlock_account_key_value = account_lockouts_ds.get(account_lockouts_key_column)
show_lockout_page
end
end
end
def get_unlock_account_key
account_lockouts_ds.get(account_lockouts_key_column)
end
def account_from_unlock_key(key)
@account = _account_from_unlock_key(key)
end
def send_unlock_account_email
@unlock_account_key_value = get_unlock_account_key
create_unlock_account_email.deliver!
end
def unlock_account_email_link
token_link(unlock_account_route, unlock_account_key_param, unlock_account_key_value)
end
private
attr_reader :unlock_account_key_value
def before_login_attempt
if locked_out?
show_lockout_page
end
super
end
def after_login
clear_invalid_login_attempts
super
end
def after_login_failure
invalid_login_attempted
super
end
def after_close_account
remove_lockout_metadata
super if defined?(super)
end
def generate_unlock_account_key
random_key
end
def remove_lockout_metadata
account_login_failures_ds.delete
account_lockouts_ds.delete
end
def show_lockout_page
set_error_flash login_lockout_error_flash
response.write unlock_account_request_view
request.halt
end
def create_unlock_account_email
create_email(unlock_account_email_subject, unlock_account_email_body)
end
def unlock_account_email_body
render('unlock-account-email')
end
def use_date_arithmetic?
db.database_type == :mysql
end
def account_login_failures_ds
db[account_login_failures_table].where(account_login_failures_id_column=>account_id)
end
def account_lockouts_ds(id=account_id)
db[account_lockouts_table].where(account_lockouts_id_column=>id)
end
def _account_from_unlock_key(token)
account_from_key(token){|id| account_lockouts_ds(id).get(account_lockouts_key_column)}
end
end
- Remember =
Feature.define(:remember) do
depends :confirm_password
notice_flash "Your remember setting has been updated"
error_flash "There was an error updating your remember setting"
view 'remember', 'Change Remember Setting'
additional_form_tags
button 'Change Remember Setting'
before
before 'load_memory'
after
after 'load_memory'
redirect
auth_value_method :remember_cookie_options, {}
auth_value_method :extend_remember_deadline?, false
auth_value_method :remember_period, {:days=>14}
auth_value_method :remembered_session_key, :remembered
auth_value_method :remember_deadline_interval, {:days=>14}
auth_value_method :remember_id_column, :id
auth_value_method :remember_key_column, :key
auth_value_method :remember_deadline_column, :deadline
auth_value_method :remember_table, :account_remember_keys
auth_value_method :remember_cookie_key, '_remember'
auth_value_method :remember_param, 'remember'
auth_value_method :remember_remember_param_value, 'remember'
auth_value_method :remember_forget_param_value, 'forget'
auth_value_method :remember_disable_param_value, 'disable'
auth_value_method :remember_remember_label, 'Remember Me'
auth_value_method :remember_forget_label, 'Forget Me'
auth_value_method :remember_disable_label, 'Disable Remember Me'
auth_methods(
:add_remember_key,
:clear_remembered_session_key,
:disable_remember_login,
:forget_login,
:generate_remember_key_value,
:get_remember_key,
:load_memory,
:logged_in_via_remember_key?,
:remember_key_value,
:remember_login,
:remove_remember_key
)
route do |r|
require_account
before_remember_route
r.get do
remember_view
end
r.post do
remember = param(remember_param)
if [remember_remember_param_value, remember_forget_param_value, remember_disable_param_value].include?(remember)
transaction do
before_remember
case remember
when remember_remember_param_value
remember_login
when remember_forget_param_value
forget_login
when remember_disable_param_value
disable_remember_login
end
after_remember
end
set_notice_flash remember_notice_flash
redirect remember_redirect
else
set_error_flash remember_error_flash
remember_view
end
end
end
def load_memory
return if session[session_key]
return unless cookie = request.cookies[remember_cookie_key]
id, key = cookie.split('_', 2)
return unless id && key
unless (actual = active_remember_key_ds(id).get(remember_key_column)) && timing_safe_eql?(key, actual)
forget_login
return
end
session[session_key] = id
account = account_from_session
session.delete(session_key)
unless account
remove_remember_key(id)
forget_login
return
end
before_load_memory
update_session
set_session_value(remembered_session_key, true)
if extend_remember_deadline?
active_remember_key_ds(id).update(remember_deadline_column=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, remember_period))
remember_login
end
after_load_memory
end
def remember_login
get_remember_key
opts = Hash[remember_cookie_options]
opts[:value] = "#{account_id}_#{remember_key_value}"
opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
::Rack::Utils.(response., remember_cookie_key, opts)
end
def forget_login
::Rack::Utils.(response., remember_cookie_key, remember_cookie_options)
end
def get_remember_key
unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
generate_remember_key_value
transaction do
remove_remember_key
add_remember_key
end
end
nil
end
def disable_remember_login
remove_remember_key
end
def add_remember_key
hash = {remember_id_column=>account_id, remember_key_column=>remember_key_value}
set_deadline_value(hash, remember_deadline_column, remember_deadline_interval)
if e = raised_uniqueness_violation{remember_key_ds.insert(hash)}
raise e unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
end
end
def remove_remember_key(id=account_id)
remember_key_ds(id).delete
end
def clear_remembered_session_key
session.delete(remembered_session_key)
end
def logged_in_via_remember_key?
!!session[remembered_session_key]
end
private
def after_logout
forget_login
super if defined?(super)
end
def after_close_account
remove_remember_key
super if defined?(super)
end
def after_confirm_password
super
clear_remembered_session_key
end
attr_reader :remember_key_value
def generate_remember_key_value
@remember_key_value = random_key
end
def use_date_arithmetic?
extend_remember_deadline? || db.database_type == :mysql
end
def remember_key_ds(id=account_id)
db[remember_table].where(remember_id_column=>id)
end
def active_remember_key_ds(id=account_id)
remember_key_ds(id).where(Sequel.expr(remember_deadline_column) > Sequel::CURRENT_TIMESTAMP)
end
end
- SmsCodes =
Feature.define(:sms_codes) do
depends :two_factor_base
additional_form_tags 'sms_auth'
additional_form_tags 'sms_confirm'
additional_form_tags 'sms_disable'
additional_form_tags 'sms_request'
additional_form_tags 'sms_setup'
before 'sms_auth'
before 'sms_confirm'
before 'sms_disable'
before 'sms_request'
before 'sms_setup'
after 'sms_confirm'
after 'sms_disable'
after 'sms_failure'
after 'sms_request'
after 'sms_setup'
button 'Authenticate via SMS Code', 'sms_auth'
button 'Confirm SMS Backup Number', 'sms_confirm'
button 'Disable Backup SMS Authentication', 'sms_disable'
button 'Send SMS Code', 'sms_request'
button 'Setup SMS Backup Number', 'sms_setup'
error_flash "Error authenticating via SMS code.", 'sms_invalid_code'
error_flash "Error disabling SMS authentication", 'sms_disable'
error_flash "Error setting up SMS authentication", 'sms_setup'
error_flash "Invalid or out of date SMS confirmation code used, must setup SMS authentication again.", 'sms_invalid_confirmation_code'
error_flash "No current SMS code for this account", 'no_current_sms_code'
error_flash "SMS authentication has been locked out.", 'sms_lockout'
error_flash "SMS authentication has already been setup.", 'sms_already_setup'
error_flash "SMS authentication has not been setup yet.", 'sms_not_setup'
error_flash "SMS authentication needs confirmation.", 'sms_needs_confirmation'
notice_flash "SMS authentication code has been sent.", 'sms_request'
notice_flash "SMS authentication has been disabled.", 'sms_disable'
notice_flash "SMS authentication has been setup.", 'sms_confirm'
redirect :sms_already_setup
redirect :sms_confirm
redirect :sms_disable
redirect(:sms_auth){"#{prefix}/#{sms_auth_route}"}
redirect(:sms_needs_confirmation){"#{prefix}/#{sms_confirm_route}"}
redirect(:sms_needs_setup){"#{prefix}/#{sms_setup_route}"}
redirect(:sms_request){"#{prefix}/#{sms_request_route}"}
view 'sms-auth', 'Authenticate via SMS Code', 'sms_auth'
view 'sms-confirm', 'Confirm SMS Backup Number', 'sms_confirm'
view 'sms-disable', 'Disable Backup SMS Authentication', 'sms_disable'
view 'sms-request', 'Send SMS Code', 'sms_request'
view 'sms-setup', 'Setup SMS Backup Number', 'sms_setup'
auth_value_method :sms_auth_code_length, 6
auth_value_method :sms_code_allowed_seconds, 300
auth_value_method :sms_code_column, :code
auth_value_method :sms_code_label, 'SMS Code'
auth_value_method :sms_code_param, 'sms-code'
auth_value_method :sms_codes_table, :account_sms_codes
auth_value_method :sms_confirm_code_length, 12
auth_value_method :sms_failure_limit, 5
auth_value_method :sms_failures_column, :num_failures
auth_value_method :sms_id_column, :id
auth_value_method :sms_invalid_code_message, "invalid SMS code"
auth_value_method :sms_invalid_phone_message, "invalid SMS phone number"
auth_value_method :sms_issued_at_column, :code_issued_at
auth_value_method :sms_phone_column, :phone_number
auth_value_method :sms_phone_label, 'Phone Number'
auth_value_method :sms_phone_min_length, 7
auth_value_method :sms_phone_param, 'sms-phone'
auth_cached_method :sms
auth_value_methods(
:sms_lockout_redirect,
:sms_codes_primary?
)
auth_methods(
:sms_auth_message,
:sms_available?,
:sms_code_issued_at,
:sms_code_match?,
:sms_confirm_message,
:sms_confirmation_match?,
:sms_current_auth?,
:sms_disable,
:sms_failures,
:sms_locked_out?,
:sms_needs_confirmation?,
:sms_new_auth_code,
:sms_new_confirm_code,
:sms_normalize_phone,
:sms_record_failure,
:sms_remove_failures,
:sms_send,
:sms_set_code,
:sms_setup,
:sms_setup?,
:sms_valid_phone?
)
route(:sms_request) do |r|
require_login
require_account_session
require_two_factor_not_authenticated
require_sms_available
before_sms_request_route
r.get do
sms_request_view
end
r.post do
transaction do
before_sms_request
sms_send_auth_code
after_sms_request
end
set_notice_flash sms_request_notice_flash
redirect sms_auth_redirect
end
end
route(:sms_auth) do |r|
require_login
require_account_session
require_two_factor_not_authenticated
require_sms_available
unless sms_current_auth?
if sms_code
sms_set_code(nil)
end
set_redirect_error_flash no_current_sms_code_error_flash
redirect sms_request_redirect
end
before_sms_auth_route
r.get do
sms_auth_view
end
r.post do
transaction do
if sms_code_match?(param(sms_code_param))
before_sms_auth
sms_remove_failures
two_factor_authenticate(:sms_code)
else
sms_record_failure
after_sms_failure
end
end
set_field_error(sms_code_param, sms_invalid_code_message)
set_error_flash sms_invalid_code_error_flash
sms_auth_view
end
end
route(:sms_setup) do |r|
require_account
unless sms_codes_primary?
require_two_factor_setup
require_two_factor_authenticated
end
require_sms_not_setup
if sms_needs_confirmation?
set_redirect_error_flash sms_needs_confirmation_error_flash
redirect sms_needs_confirmation_redirect
end
before_sms_setup_route
r.get do
sms_setup_view
end
r.post do
catch_error do
unless two_factor_password_match?(param(password_param))
throw_error(password_param, invalid_password_message)
end
phone = sms_normalize_phone(param(sms_phone_param))
unless sms_valid_phone?(phone)
throw_error(sms_phone_param, sms_invalid_phone_message)
end
transaction do
before_sms_setup
sms_setup(phone)
sms_send_confirm_code
after_sms_setup
end
set_notice_flash sms_needs_confirmation_error_flash
redirect sms_needs_confirmation_redirect
end
set_error_flash sms_setup_error_flash
sms_setup_view
end
end
route(:sms_confirm) do |r|
require_account
unless sms_codes_primary?
require_two_factor_setup
require_two_factor_authenticated
end
require_sms_not_setup
before_sms_confirm_route
r.get do
sms_confirm_view
end
r.post do
if sms_confirmation_match?(param(sms_code_param))
transaction do
before_sms_confirm
sms_confirm
after_sms_confirm
if sms_codes_primary?
two_factor_authenticate(:sms_code)
end
end
set_notice_flash sms_confirm_notice_flash
redirect sms_confirm_redirect
end
sms_confirm_failure
set_redirect_error_flash sms_invalid_confirmation_code_error_flash
redirect sms_needs_setup_redirect
end
end
route(:sms_disable) do |r|
require_account
require_sms_setup
before_sms_disable_route
r.get do
sms_disable_view
end
r.post do
if two_factor_password_match?(param(password_param))
transaction do
before_sms_disable
sms_disable
if sms_codes_primary?
two_factor_remove_session
end
after_sms_disable
end
set_notice_flash sms_disable_notice_flash
redirect sms_disable_redirect
end
set_field_error(password_param, invalid_password_message)
set_error_flash sms_disable_error_flash
sms_disable_view
end
end
def two_factor_need_setup_redirect
super || (sms_needs_setup_redirect if sms_codes_primary?)
end
def two_factor_auth_required_redirect
super || (sms_request_redirect if sms_codes_primary? && sms_available?)
end
def two_factor_auth_fallback_redirect
sms_available? ? sms_request_redirect : super
end
def two_factor_remove
super
sms_disable
end
def two_factor_remove_auth_failures
super
sms_remove_failures
end
def two_factor_authentication_setup?
super || (sms_codes_primary? && sms_setup?)
end
def
"#{super if defined?(super)}#{"<p><a href=\"#{sms_request_route}\">Authenticate using SMS code</a></p>" if sms_available?}"
end
def otp_lockout_redirect
if sms_available?
sms_request_redirect
else
super if defined?(super)
end
end
def otp_lockout_error_flash
msg = super if defined?(super)
if sms_available?
msg = "#{msg} Can use SMS code to unlock."
end
msg
end
def otp_remove
super if defined?(super)
unless sms_codes_primary?
sms_disable
end
end
def require_sms_setup
unless sms_setup?
set_redirect_error_flash sms_not_setup_error_flash
redirect sms_needs_setup_redirect
end
end
def require_sms_not_setup
if sms_setup?
set_redirect_error_flash sms_already_setup_error_flash
redirect sms_already_setup_redirect
end
end
def require_sms_available
require_sms_setup
if sms_locked_out?
set_redirect_error_flash sms_lockout_error_flash
redirect sms_lockout_redirect
end
end
def sms_code_match?(code)
return false unless sms_current_auth?
timing_safe_eql?(code, sms_code)
end
def sms_confirmation_match?(code)
sms_needs_confirmation? && sms_code_match?(code)
end
def sms_disable
sms_ds.delete
@sms = nil
super if defined?(super)
end
def sms_confirm_failure
sms_ds.delete
end
def sms_setup(phone_number)
sms_ds.insert(sms_id_column=>session_value, sms_phone_column=>phone_number)
remove_instance_variable(:@sms) if instance_variable_defined?(:@sms)
end
def sms_remove_failures
update_sms(sms_failures_column => 0, sms_code_column => nil)
end
def sms_confirm
sms_remove_failures
super if defined?(super)
end
def sms_send_auth_code
code = sms_new_auth_code
sms_set_code(code)
sms_send(sms_phone, sms_auth_message(code))
end
def sms_send_confirm_code
code = sms_new_confirm_code
sms_set_code(code)
sms_send(sms_phone, sms_confirm_message(code))
end
def sms_valid_phone?(phone)
phone.length >= sms_phone_min_length
end
def sms_lockout_redirect
_two_factor_auth_required_redirect
end
def sms_auth_message(code)
"SMS authentication code for #{request.host} is #{code}"
end
def sms_confirm_message(code)
"SMS confirmation code for #{request.host} is #{code}"
end
def sms_set_code(code)
update_sms(sms_code_column=>code, sms_issued_at_column=>Sequel::CURRENT_TIMESTAMP)
end
def sms_record_failure
update_sms(sms_failures_column=>Sequel.expr(sms_failures_column)+1)
sms[sms_failures_column] = sms_ds.get(sms_failures_column)
end
def sms_phone
sms[sms_phone_column]
end
def sms_code
sms[sms_code_column]
end
def sms_code_issued_at
convert_timestamp(sms[sms_issued_at_column])
end
def sms_failures
sms[sms_failures_column]
end
def sms_setup?
return false unless sms
!sms_needs_confirmation?
end
def sms_needs_confirmation?
sms && sms_failures.nil?
end
def sms_available?
sms && !sms_needs_confirmation? && !sms_locked_out?
end
def sms_locked_out?
sms_failures >= sms_failure_limit
end
def sms_current_auth?
sms_code && sms_code_issued_at + sms_code_allowed_seconds > Time.now
end
private
def sms_codes_primary?
!features.include?(:otp)
end
def sms_normalize_phone(phone)
phone.to_s.gsub(/\D+/, '')
end
def sms_new_auth_code
SecureRandom.random_number(10**sms_auth_code_length).to_s.rjust(sms_auth_code_length, "0")
end
def sms_new_confirm_code
SecureRandom.random_number(10**sms_confirm_code_length).to_s.rjust(sms_confirm_code_length, "0")
end
def sms_send(phone, message)
raise NotImplementedError, "sms_send needs to be defined in the Rodauth configuration for SMS sending to work"
end
def update_sms(values)
update_hash_ds(sms, sms_ds, values)
end
def _sms
sms_ds.first
end
def sms_ds
db[sms_codes_table].where(sms_id_column=>session_value)
end
end
- EmailBase =
Feature.define(:email_base) do
auth_value_method :email_subject_prefix, nil
auth_value_method :require_mail?, true
auth_value_method :token_separator, "_"
auth_value_methods(
:email_from
)
auth_methods(
:create_email,
:email_to
)
def post_configure
super
require 'mail' if require_mail?
end
private
def create_email(subject, body)
m = Mail.new
m.from = email_from
m.to = email_to
m.subject = "#{email_subject_prefix}#{subject}"
m.body = body
m
end
def email_from
"webmaster@#{request.host}"
end
def email_to
account[login_column]
end
def split_token(token)
token.split(token_separator, 2)
end
def token_link(route, param, key)
"#{request.base_url}#{prefix}/#{route}?#{param}=#{account_id}#{token_separator}#{key}"
end
def account_from_key(token, status_id=nil)
id, key = split_token(token)
return unless id && key
return unless actual = yield(id)
return unless timing_safe_eql?(key, actual)
ds = account_ds(id)
ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
ds.first
end
end
- ChangeLogin =
Feature.define(:change_login) do
depends :login_password_requirements_base
notice_flash 'Your login has been changed'
error_flash 'There was an error changing your login'
view 'change-login', 'Change Login'
after
before
additional_form_tags
button 'Change Login'
redirect
auth_value_methods :change_login_requires_password?
auth_methods :change_login
route do |r|
require_account
before_change_login_route
r.get do
change_login_view
end
r.post do
catch_error do
if change_login_requires_password? && !password_match?(param(password_param))
throw_error(password_param, invalid_password_message)
end
login = param(login_param)
unless login_meets_requirements?(login)
throw_error(login_param, login_does_not_meet_requirements_message)
end
if require_login_confirmation? && login != param(login_confirm_param)
throw_error(login_param, logins_do_not_match_message)
end
transaction do
before_change_login
unless change_login(login)
throw_error(login_param, login_does_not_meet_requirements_message)
end
after_change_login
set_notice_flash change_login_notice_flash
redirect change_login_redirect
end
end
set_error_flash change_login_error_flash
change_login_view
end
end
def change_login_requires_password?
modifications_require_password?
end
def change_login(login)
updated = nil
if account_ds.get(login_column).downcase == login.downcase
@login_requirement_message = 'same as current login'
return false
end
raised = raises_uniqueness_violation?{updated = update_account({login_column=>login}, account_ds.exclude(login_column=>login)) == 1}
if raised
@login_requirement_message = 'already an account with this login'
end
updated && !raised
end
end
- CloseAccount =
Feature.define(:close_account) do
notice_flash 'Your account has been closed'
error_flash 'There was an error closing your account'
view 'close-account', 'Close Account'
additional_form_tags
button 'Close Account'
after
before
redirect
auth_value_method :account_closed_status_value, 3
auth_value_methods(
:close_account_requires_password?,
:delete_account_on_close?
)
auth_methods(
:close_account,
:delete_account
)
route do |r|
require_account
before_close_account_route
r.get do
close_account_view
end
r.post do
if !close_account_requires_password? || password_match?(param(password_param))
transaction do
before_close_account
close_account
after_close_account
if delete_account_on_close?
delete_account
end
end
clear_session
set_notice_flash close_account_notice_flash
redirect close_account_redirect
else
set_field_error(password_param, invalid_password_message)
set_error_flash close_account_error_flash
close_account_view
end
end
end
def close_account_requires_password?
modifications_require_password?
end
def close_account
unless skip_status_checks?
update_account(account_status_column=>account_closed_status_value)
end
unless account_password_hash_column
password_hash_ds.delete
end
end
def delete_account
account_ds.delete
end
def delete_account_on_close?
skip_status_checks?
end
def skip_status_checks?
false
end
end
- CreateAccount =
Feature.define(:create_account) do
depends :login_password_requirements_base
depends :login
notice_flash 'Your account has been created'
error_flash "There was an error creating your account"
view 'create-account', 'Create Account'
after
before
button 'Create Account'
additional_form_tags
redirect
auth_value_method :create_account_autologin?, true
auth_value_methods :create_account_link
auth_methods(
:save_account,
:set_new_account_password
)
auth_private_methods(
:new_account
)
route do |r|
check_already_logged_in
before_create_account_route
r.get do
create_account_view
end
r.post do
login = param(login_param)
password = param(password_param)
new_account(login)
if account_password_hash_column
set_new_account_password(param(password_param))
end
catch_error do
if require_login_confirmation? && login != param(login_confirm_param)
throw_error(login_param, logins_do_not_match_message)
end
unless login_meets_requirements?(login)
throw_error(login_param, login_does_not_meet_requirements_message)
end
if require_password_confirmation? && password != param(password_confirm_param)
throw_error(password_param, passwords_do_not_match_message)
end
unless password_meets_requirements?(password)
throw_error(password_param, password_does_not_meet_requirements_message)
end
transaction do
before_create_account
unless save_account
throw_error(login_param, login_does_not_meet_requirements_message)
end
unless account_password_hash_column
set_password(password)
end
after_create_account
if create_account_autologin?
update_session
end
set_notice_flash create_account_notice_flash
redirect create_account_redirect
end
end
set_error_flash create_account_error_flash
create_account_view
end
end
def create_account_link
"<p><a href=\"#{prefix}/#{create_account_route}\">Create a New Account</a></p>"
end
def
super + create_account_link
end
def set_new_account_password(password)
account[account_password_hash_column] = password_hash(password)
end
def new_account(login)
@account = _new_account(login)
end
def save_account
id = nil
raised = raises_uniqueness_violation?{id = db[accounts_table].insert(account)}
if raised
@login_requirement_message = 'already an account with this login'
end
if id
account[account_id_column] = id
end
id && !raised
end
private
def _new_account(login)
acc = {login_column=>login}
unless skip_status_checks?
acc[account_status_column] = account_initial_status_value
end
acc
end
end
- RecoveryCodes =
Feature.define(:recovery_codes) do
depends :two_factor_base
additional_form_tags 'recovery_auth'
additional_form_tags 'recovery_codes'
before 'add_recovery_codes'
before 'view_recovery_codes'
before 'recovery_auth'
before 'recovery_auth_route'
before 'recovery_codes_route'
after 'add_recovery_codes'
button 'Add Authentication Recovery Codes', 'add_recovery_codes'
button 'Authenticate via Recovery Code', 'recovery_auth'
button 'View Authentication Recovery Codes', 'view_recovery_codes'
error_flash "Error authenticating via recovery code.", 'invalid_recovery_code'
error_flash "Unable to add recovery codes.", 'add_recovery_codes'
error_flash "Unable to view recovery codes.", 'view_recovery_codes'
notice_flash "Additional authentication recovery codes have been added.", 'recovery_codes_added'
redirect(:recovery_auth){"#{prefix}/#{recovery_auth_route}"}
redirect(:add_recovery_codes){"#{prefix}/#{recovery_codes_route}"}
view 'add-recovery-codes', 'Authentication Recovery Codes', 'add_recovery_codes'
view 'recovery-auth', 'Enter Authentication Recovery Code', 'recovery_auth'
view 'recovery-codes', 'View Authentication Recovery Codes', 'recovery_codes'
auth_value_method :add_recovery_codes_param, 'add'
auth_value_method :invalid_recovery_code_message, "Invalid recovery code"
auth_value_method :recovery_codes_limit, 16
auth_value_method :recovery_codes_column, :code
auth_value_method :recovery_codes_id_column, :id
auth_value_method :recovery_codes_label, 'Recovery Code'
auth_value_method :recovery_codes_param, 'recovery-code'
auth_value_method :recovery_codes_table, :account_recovery_codes
auth_cached_method :recovery_codes
auth_value_methods(
:recovery_codes_primary?
)
auth_methods(
:add_recovery_code,
:can_add_recovery_codes?,
:new_recovery_code,
:recovery_code_match?,
:recovery_codes
)
route(:recovery_auth) do |r|
require_login
require_account_session
require_two_factor_setup
require_two_factor_not_authenticated
before_recovery_auth_route
r.get do
recovery_auth_view
end
r.post do
if recovery_code_match?(param(recovery_codes_param))
before_recovery_auth
two_factor_authenticate(:recovery_code)
end
set_field_error(recovery_codes_param, invalid_recovery_code_message)
set_error_flash invalid_recovery_code_error_flash
recovery_auth_view
end
end
route(:recovery_codes) do |r|
require_account
unless recovery_codes_primary?
require_two_factor_setup
require_two_factor_authenticated
end
before_recovery_codes_route
r.get do
recovery_codes_view
end
r.post do
if two_factor_password_match?(param(password_param))
if can_add_recovery_codes?
if param_or_nil(add_recovery_codes_param)
transaction do
before_add_recovery_codes
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
after_add_recovery_codes
end
set_notice_now_flash recovery_codes_added_notice_flash
end
self.recovery_codes_button = add_recovery_codes_button
end
before_view_recovery_codes
add_recovery_codes_view
else
if param_or_nil(add_recovery_codes_param)
set_error_flash add_recovery_codes_error_flash
else
set_error_flash view_recovery_codes_error_flash
end
set_field_error(password_param, invalid_password_message)
recovery_codes_view
end
end
end
attr_accessor :recovery_codes_button
def two_factor_need_setup_redirect
super || (add_recovery_codes_redirect if recovery_codes_primary?)
end
def two_factor_auth_required_redirect
super || (recovery_auth_redirect if recovery_codes_primary?)
end
def two_factor_auth_fallback_redirect
recovery_auth_redirect
end
def two_factor_remove
super
recovery_codes_remove
end
def two_factor_authentication_setup?
super || (recovery_codes_primary? && !recovery_codes.empty?)
end
def
"#{super if defined?(super)}<p><a href=\"#{recovery_auth_route}\">Authenticate using recovery code</a></p>"
end
def otp_lockout_redirect
recovery_auth_redirect
end
def otp_lockout_error_flash
"#{super if defined?(super)} Can use recovery code to unlock."
end
def otp_add_key
super if defined?(super)
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
end
def sms_confirm
super if defined?(super)
add_recovery_codes(recovery_codes_limit - recovery_codes.length)
end
def otp_remove
super if defined?(super)
unless recovery_codes_primary?
recovery_codes_remove
end
end
def sms_disable
super if defined?(super)
unless recovery_codes_primary?
recovery_codes_remove
end
end
def recovery_codes_remove
recovery_codes_ds.delete
end
def recovery_code_match?(code)
recovery_codes.each do |s|
if timing_safe_eql?(code, s)
recovery_codes_ds.where(recovery_codes_column=>code).delete
if recovery_codes_primary?
add_recovery_code
end
return true
end
end
false
end
def can_add_recovery_codes?
recovery_codes.length < recovery_codes_limit
end
def add_recovery_codes(number)
return if number <= 0
transaction do
number.times do
add_recovery_code
end
end
remove_instance_variable(:@recovery_codes)
end
def add_recovery_code
retry_on_uniqueness_violation do
recovery_codes_ds.insert(recovery_codes_id_column=>session_value, recovery_codes_column=>new_recovery_code)
end
end
private
def new_recovery_code
random_key
end
def recovery_codes_primary?
(features & [:otp, :sms_codes]).empty?
end
def _recovery_codes
recovery_codes_ds.select_map(recovery_codes_column)
end
def recovery_codes_ds
db[recovery_codes_table].where(recovery_codes_id_column=>session_value)
end
end
- ResetPassword =
Feature.define(:reset_password) do
depends :login, :email_base, :login_password_requirements_base
notice_flash "Your password has been reset"
notice_flash "An email has been sent to you with a link to reset the password for your account", 'reset_password_email_sent'
error_flash "There was an error resetting your password"
error_flash "There was an error requesting a password reset", 'reset_password_request'
view 'reset-password', 'Reset Password'
additional_form_tags
additional_form_tags 'reset_password_request'
before
before 'reset_password_request'
after
after 'reset_password_request'
button 'Reset Password'
button 'Request Password Reset', 'reset_password_request'
redirect
redirect :reset_password_email_sent
auth_value_method :reset_password_deadline_column, :deadline
auth_value_method :reset_password_deadline_interval, {:days=>1}
auth_value_method :no_matching_reset_password_key_message, "invalid password reset key"
auth_value_method :reset_password_email_subject, 'Reset Password'
auth_value_method :reset_password_key_param, 'key'
auth_value_method :reset_password_autologin?, false
auth_value_method :reset_password_table, :account_password_reset_keys
auth_value_method :reset_password_id_column, :id
auth_value_method :reset_password_key_column, :key
auth_value_methods :reset_password_email_sent_redirect
auth_methods(
:create_reset_password_key,
:create_reset_password_email,
:get_reset_password_key,
:remove_reset_password_key,
:reset_password_email_body,
:reset_password_email_link,
:reset_password_key_insert_hash,
:reset_password_key_value,
:send_reset_password_email
)
auth_private_methods(
:account_from_reset_password_key
)
route(:reset_password_request) do |r|
check_already_logged_in
before_reset_password_request_route
r.post do
if account_from_login(param(login_param)) && open_account?
generate_reset_password_key_value
transaction do
before_reset_password_request
create_reset_password_key
send_reset_password_email
after_reset_password_request
end
set_notice_flash reset_password_email_sent_notice_flash
else
set_redirect_error_flash reset_password_request_error_flash
end
redirect reset_password_email_sent_redirect
end
end
route do |r|
check_already_logged_in
before_reset_password_route
r.get do
if key = param_or_nil(reset_password_key_param)
if account_from_reset_password_key(key)
reset_password_view
else
set_redirect_error_flash no_matching_reset_password_key_message
redirect require_login_redirect
end
end
end
r.post do
key = param(reset_password_key_param)
unless account_from_reset_password_key(key)
set_redirect_error_flash reset_password_error_flash
redirect reset_password_email_sent_redirect
end
password = param(password_param)
catch_error do
if password_match?(password)
throw_error(password_param, same_as_existing_password_message)
end
if require_password_confirmation? && password != param(password_confirm_param)
throw_error(password_param, passwords_do_not_match_message)
end
unless password_meets_requirements?(password)
throw_error(password_param, password_does_not_meet_requirements_message)
end
transaction do
before_reset_password
set_password(password)
remove_reset_password_key
after_reset_password
end
if reset_password_autologin?
update_session
end
set_notice_flash reset_password_notice_flash
redirect reset_password_redirect
end
set_error_flash reset_password_error_flash
reset_password_view
end
end
def create_reset_password_key
ds = password_reset_ds
transaction do
ds.where(Sequel::CURRENT_TIMESTAMP > reset_password_deadline_column).delete
if ds.empty?
if e = raised_uniqueness_violation{ds.insert(reset_password_key_insert_hash)}
raise e unless @reset_password_key_value = get_password_reset_key(account_id)
end
end
end
end
def remove_reset_password_key
password_reset_ds.delete
end
def account_from_reset_password_key(key)
@account = _account_from_reset_password_key(key)
end
def send_reset_password_email
create_reset_password_email.deliver!
end
def reset_password_email_link
token_link(reset_password_route, reset_password_key_param, reset_password_key_value)
end
def get_password_reset_key(id)
password_reset_ds(id).get(reset_password_key_column)
end
private
attr_reader :reset_password_key_value
def after_login_failure
@login_form_header = render("reset-password-request")
super
end
def after_close_account
remove_reset_password_key
super if defined?(super)
end
def generate_reset_password_key_value
@reset_password_key_value = random_key
end
def create_reset_password_email
create_email(reset_password_email_subject, reset_password_email_body)
end
def reset_password_email_body
render('reset-password-email')
end
def use_date_arithmetic?
db.database_type == :mysql
end
def reset_password_key_insert_hash
hash = {reset_password_id_column=>account_id, reset_password_key_column=>reset_password_key_value}
set_deadline_value(hash, reset_password_deadline_column, reset_password_deadline_interval)
hash
end
def password_reset_ds(id=account_id)
db[reset_password_table].where(reset_password_id_column=>id)
end
def _account_from_reset_password_key(token)
account_from_key(token, account_open_status_value){|id| get_password_reset_key(id)}
end
end
- SingleSession =
Feature.define(:single_session) do
error_flash 'This session has been logged out as another session has become active'
redirect
auth_value_method :single_session_id_column, :id
auth_value_method :single_session_key_column, :key
auth_value_method :single_session_session_key, :single_session_key
auth_value_method :single_session_table, :account_session_keys
auth_methods(
:currently_active_session?,
:no_longer_active_session,
:reset_single_session_key,
:update_single_session_key
)
def reset_single_session_key
if logged_in?
single_session_ds.update(single_session_key_column=>random_key)
end
end
def currently_active_session?
single_session_key = session[single_session_session_key]
current_key = single_session_ds.get(single_session_key_column)
if single_session_key.nil?
unless current_key
update_single_session_key
end
true
elsif current_key
timing_safe_eql?(single_session_key, current_key)
end
end
def check_single_session
if logged_in? && !currently_active_session?
no_longer_active_session
end
end
def no_longer_active_session
clear_session
set_redirect_error_flash single_session_error_flash
redirect single_session_redirect
end
def update_single_session_key
key = random_key
set_session_value(single_session_session_key, key)
if single_session_ds.update(single_session_key_column=>key) == 0
single_session_ds.insert(single_session_id_column=>session_value, single_session_key_column=>key)
end
end
private
def after_close_account
super if defined?(super)
single_session_ds.delete
end
def before_logout
reset_single_session_key if request.post?
super if defined?(super)
end
def update_session
super
update_single_session_key
end
def single_session_ds
db[single_session_table].
where(single_session_id_column=>session_value)
end
end
- VerifyAccount =
Feature.define(:verify_account) do
depends :login, :create_account, :email_base
error_flash "Unable to verify account"
error_flash "Unable to resend verify account email", 'verify_account_resend'
notice_flash "Your account has been verified"
notice_flash "An email has been sent to you with a link to verify your account", 'verify_account_email_sent'
view 'verify-account', 'Verify Account'
view 'verify-account-resend', 'Resend Verification Email', 'resend_verify_account'
additional_form_tags
additional_form_tags 'verify_account_resend'
after
after 'verify_account_email_resend'
before
before 'verify_account_email_resend'
button 'Verify Account'
button 'Send Verification Email Again', 'verify_account_resend'
redirect
redirect(:verify_account_email_sent){require_login_redirect}
auth_value_method :no_matching_verify_account_key_message, "invalid verify account key"
auth_value_method :attempt_to_create_unverified_account_notice_message, "The account you tried to create is currently awaiting verification"
auth_value_method :attempt_to_login_to_unverified_account_notice_message, "The account you tried to login with is currently awaiting verification"
auth_value_method :verify_account_email_subject, 'Verify Account'
auth_value_method :verify_account_key_param, 'key'
auth_value_method :verify_account_autologin?, true
auth_value_method :verify_account_table, :account_verification_keys
auth_value_method :verify_account_id_column, :id
auth_value_method :verify_account_key_column, :key
auth_value_methods :verify_account_key_value
auth_methods(
:create_verify_account_key,
:create_verify_account_email,
:get_verify_account_key,
:remove_verify_account_key,
:resend_verify_account_view,
:send_verify_account_email,
:verify_account,
:verify_account_email_body,
:verify_account_email_link,
:verify_account_key_insert_hash
)
auth_private_methods(
:account_from_verify_account_key
)
route(:verify_account_resend) do |r|
verify_account_check_already_logged_in
before_verify_account_resend_route
r.post do
if account_from_login(param(login_param)) && !open_account?
before_verify_account_email_resend
if verify_account_email_resend
after_verify_account_email_resend
end
set_notice_flash verify_account_email_sent_notice_flash
else
set_redirect_error_flash verify_account_resend_error_flash
end
redirect verify_account_email_sent_redirect
end
end
route do |r|
verify_account_check_already_logged_in
before_verify_account_route
r.get do
if key = param_or_nil(verify_account_key_param)
if account_from_verify_account_key(key)
verify_account_view
else
set_redirect_error_flash no_matching_verify_account_key_message
redirect require_login_redirect
end
end
end
r.post do
key = param(verify_account_key_param)
unless account_from_verify_account_key(key)
set_redirect_error_flash verify_account_error_flash
redirect verify_account_redirect
end
transaction do
before_verify_account
verify_account
remove_verify_account_key
after_verify_account
end
if verify_account_autologin?
update_session
end
set_notice_flash verify_account_notice_flash
redirect verify_account_redirect
end
end
def remove_verify_account_key
verify_account_ds.delete
end
def verify_account
update_account(account_status_column=>account_open_status_value) == 1
end
def verify_account_email_resend
if @verify_account_key_value = get_verify_account_key(account_id)
send_verify_account_email
true
end
end
def create_account_notice_flash
verify_account_email_sent_notice_flash
end
def new_account(login)
if account_from_login(login)
set_error_flash attempt_to_create_unverified_account_notice_message
response.write resend_verify_account_view
request.halt
end
super
end
def account_from_verify_account_key(key)
@account = _account_from_verify_account_key(key)
end
def account_initial_status_value
account_unverified_status_value
end
def send_verify_account_email
create_verify_account_email.deliver!
end
def verify_account_email_link
token_link(verify_account_route, verify_account_key_param, verify_account_key_value)
end
def get_verify_account_key(id)
verify_account_ds(id).get(verify_account_key_column)
end
def skip_status_checks?
false
end
def create_account_autologin?
false
end
private
attr_reader :verify_account_key_value
def before_login_attempt
unless open_account?
set_error_flash attempt_to_login_to_unverified_account_notice_message
response.write resend_verify_account_view
request.halt
end
super
end
def after_create_account
setup_account_verification
super
end
def setup_account_verification
generate_verify_account_key_value
create_verify_account_key
send_verify_account_email
end
def verify_account_check_already_logged_in
check_already_logged_in
end
def generate_verify_account_key_value
@verify_account_key_value = random_key
end
def create_verify_account_key
ds = verify_account_ds
transaction do
if ds.empty?
if e = raised_uniqueness_violation{ds.insert(verify_account_key_insert_hash)}
raise e unless @verify_account_key_value = get_verify_account_key(account_id)
end
end
end
end
def verify_account_key_insert_hash
{verify_account_id_column=>account_id, verify_account_key_column=>verify_account_key_value}
end
def create_verify_account_email
create_email(verify_account_email_subject, verify_account_email_body)
end
def verify_account_email_body
render('verify-account-email')
end
def verify_account_ds(id=account_id)
db[verify_account_table].where(verify_account_id_column=>id)
end
def _account_from_verify_account_key(token)
account_from_key(token, account_unverified_status_value){|id| get_verify_account_key(id)}
end
end
- ChangePassword =
Feature.define(:change_password) do
depends :login_password_requirements_base
notice_flash 'Your password has been changed'
error_flash 'There was an error changing your password'
view 'change-password', 'Change Password'
after
before
additional_form_tags
button 'Change Password'
redirect
auth_value_method :new_password_label, 'New Password'
auth_value_method :new_password_param, 'new-password'
auth_value_methods :change_password_requires_password?
route do |r|
require_account
before_change_password_route
r.get do
change_password_view
end
r.post do
catch_error do
if change_password_requires_password? && !password_match?(param(password_param))
throw_error(password_param, invalid_password_message)
end
password = param(new_password_param)
if require_password_confirmation? && password != param(password_confirm_param)
throw_error(new_password_param, passwords_do_not_match_message)
end
if password_match?(password)
throw_error(new_password_param, same_as_existing_password_message)
end
unless password_meets_requirements?(password)
throw_error(new_password_param, password_does_not_meet_requirements_message)
end
transaction do
before_change_password
set_password(password)
after_change_password
end
set_notice_flash change_password_notice_flash
redirect change_password_redirect
end
set_error_flash change_password_error_flash
change_password_view
end
end
def change_password_requires_password?
modifications_require_password?
end
end
- TwoFactorBase =
Feature.define(:two_factor_base) do
after :two_factor_authentication
redirect :two_factor_auth
redirect :two_factor_already_authenticated
notice_flash "You have been authenticated via 2nd factor", "two_factor_auth"
error_flash "This account has not been setup for two factor authentication", 'two_factor_not_setup'
error_flash "Already authenticated via 2nd factor", 'two_factor_already_authenticated'
error_flash "You need to authenticate via 2nd factor before continuing.", 'two_factor_need_authentication'
auth_value_method :two_factor_session_key, :two_factor_auth
auth_value_method :two_factor_setup_session_key, :two_factor_auth_setup
auth_value_method :two_factor_need_setup_redirect, nil
auth_value_methods(
:two_factor_auth_required_redirect,
:two_factor_modifications_require_password?
)
auth_methods(
:two_factor_authenticated?,
:two_factor_remove,
:two_factor_remove_auth_failures,
:two_factor_remove_session,
:two_factor_update_session
)
def two_factor_modifications_require_password?
modifications_require_password?
end
def authenticated?
super
two_factor_authenticated? if two_factor_authentication_setup?
end
def require_authentication
super
require_two_factor_authenticated if two_factor_authentication_setup?
end
def require_two_factor_setup
unless uses_two_factor_authentication?
set_redirect_error_flash two_factor_not_setup_error_flash
redirect two_factor_need_setup_redirect
end
end
def require_two_factor_not_authenticated
if two_factor_authenticated?
set_redirect_error_flash two_factor_already_authenticated_error_flash
redirect two_factor_already_authenticated_redirect
end
end
def require_two_factor_authenticated
unless two_factor_authenticated?
set_redirect_error_flash two_factor_need_authentication_error_flash
redirect _two_factor_auth_required_redirect
end
end
def two_factor_remove_auth_failures
nil
end
def two_factor_auth_required_redirect
nil
end
def two_factor_auth_fallback_redirect
nil
end
def two_factor_password_match?(password)
if two_factor_modifications_require_password?
password_match?(password)
else
true
end
end
def two_factor_authenticated?
!!session[two_factor_session_key]
end
def two_factor_authentication_setup?
false
end
def uses_two_factor_authentication?
return false unless logged_in?
session[two_factor_setup_session_key] = two_factor_authentication_setup? unless session.has_key?(two_factor_setup_session_key)
session[two_factor_setup_session_key]
end
def two_factor_remove
nil
end
private
def after_close_account
super if defined?(super)
two_factor_remove
end
def two_factor_authenticate(type)
two_factor_update_session(type)
two_factor_remove_auth_failures
after_two_factor_authentication
set_notice_flash two_factor_auth_notice_flash
redirect two_factor_auth_redirect
end
def two_factor_remove_session
session.delete(two_factor_session_key)
session[two_factor_setup_session_key] = false
end
def two_factor_update_session(type)
session[two_factor_session_key] = type
session[two_factor_setup_session_key] = true
end
def _two_factor_auth_required_redirect
two_factor_auth_required_redirect || two_factor_auth_fallback_redirect || default_redirect
end
end
- ConfirmPassword =
Feature.define(:confirm_password) do
notice_flash "Your password has been confirmed"
error_flash "There was an error confirming your password"
view 'confirm-password', 'Confirm Password'
additional_form_tags
button 'Confirm Password'
before
after
auth_value_methods :confirm_password_redirect
auth_methods :confirm_password
route do
require_account
before_confirm_password_route
request.get do
confirm_password_view
end
request.post do
if password_match?(param(password_param))
transaction do
before_confirm_password
confirm_password
after_confirm_password
end
set_notice_flash confirm_password_notice_flash
redirect confirm_password_redirect
else
set_field_error(password_param, invalid_password_message)
set_error_flash confirm_password_error_flash
confirm_password_view
end
end
end
def confirm_password
nil
end
def confirm_password_redirect
session.delete(:confirm_password_redirect) || default_redirect
end
end
- AccountExpiration =
Feature.define(:account_expiration) do
error_flash "You cannot log into this account as it has expired"
redirect
after
auth_value_method :account_activity_expired_column, :expired_at
auth_value_method :account_activity_id_column, :id
auth_value_method :account_activity_last_activity_column, :last_activity_at
auth_value_method :account_activity_last_login_column, :last_login_at
auth_value_method :account_activity_table, :account_activity_times
auth_value_method :expire_account_after, 180*86400
auth_value_method :expire_account_on_last_activity?, false
auth_methods(
:account_expired?,
:account_expired_at,
:last_account_activity_at,
:last_account_login_at,
:set_expired,
:update_last_activity,
:update_last_login
)
def last_account_activity_at
get_activity_timestamp(session_value, account_activity_last_activity_column)
end
def last_account_login_at
get_activity_timestamp(session_value, account_activity_last_login_column)
end
def account_expired_at
get_activity_timestamp(account_id, account_activity_expired_column)
end
def update_last_login
update_activity(account_id, account_activity_last_login_column, account_activity_last_activity_column)
end
def update_last_activity
if session_value
update_activity(session_value, account_activity_last_activity_column)
end
end
def set_expired
update_activity(account_id, account_activity_expired_column)
after_account_expiration
end
def account_expired?
columns = [account_activity_last_activity_column, account_activity_last_login_column, account_activity_expired_column]
last_activity, last_login, expired = account_activity_ds(account_id).get(columns)
return true if expired
timestamp = convert_timestamp(expire_account_on_last_activity? ? last_activity : last_login)
return false unless timestamp
timestamp < Time.now - expire_account_after
end
def check_account_expiration
if account_expired?
set_expired unless account_expired_at
set_redirect_error_flash account_expiration_error_flash
redirect account_expiration_redirect
end
update_last_login
end
private
def after_close_account
super if defined?(super)
account_activity_ds(account_id).delete
end
def update_session
check_account_expiration
super
end
def account_activity_ds(account_id)
db[account_activity_table].
where(account_activity_id_column=>account_id)
end
def get_activity_timestamp(account_id, column)
convert_timestamp(account_activity_ds(account_id).get(column))
end
def update_activity(account_id, *columns)
ds = account_activity_ds(account_id)
hash = {}
columns.each do |c|
hash[c] = Sequel::CURRENT_TIMESTAMP
end
if ds.update(hash) == 0
hash[account_activity_id_column] = account_id
hash[account_activity_last_activity_column] ||= Sequel::CURRENT_TIMESTAMP
hash[account_activity_last_login_column] ||= Sequel::CURRENT_TIMESTAMP
ignore_uniqueness_violation{ds.insert(hash)}
end
end
end
- SessionExpiration =
Feature.define(:session_expiration) do
error_flash "This session has expired, please login again."
auth_value_method :max_session_lifetime, 86400
auth_value_method :session_created_session_key, :session_created_at
auth_value_method :session_expiration_default, true
auth_value_method :session_inactivity_timeout, 1800
auth_value_method :session_last_activity_session_key, :last_session_activity_at
auth_value_methods :session_expiration_redirect
def check_session_expiration
return unless logged_in?
unless session.has_key?(session_last_activity_session_key) && session.has_key?(session_created_session_key)
if session_expiration_default
expire_session
end
return
end
time = Time.now.to_i
if session[session_last_activity_session_key] + session_inactivity_timeout < time
expire_session
end
set_session_value(session_last_activity_session_key, time)
if session[session_created_session_key] + max_session_lifetime < time
expire_session
end
end
def expire_session
clear_session
set_redirect_error_flash session_expiration_error_flash
redirect session_expiration_redirect
end
def session_expiration_redirect
require_login_redirect
end
private
def update_session
super
session[session_last_activity_session_key] = session[session_created_session_key] = Time.now.to_i
end
end
- PasswordComplexity =
Feature.define(:password_complexity) do
depends :login_password_requirements_base
auth_value_method :password_dictionary_file, nil
auth_value_method :password_dictionary, nil
auth_value_method :password_character_groups, [/[a-z]/, /[A-Z]/, /\d/, /[^a-zA-Z\d]/]
auth_value_method :password_min_groups, 3
auth_value_method :password_max_length_for_groups_check, 11
auth_value_method :password_max_repeating_characters, 3
auth_value_method :password_invalid_pattern, Regexp.union([/qwerty/i, /azerty/i, /asdf/i, /zxcv/i] + (1..8).map{|i| /#{i}#{i+1}#{(i+2)%10}/})
auth_value_method :password_not_enough_character_groups_message, "does not include uppercase letters, lowercase letters, and numbers"
auth_value_method :password_invalid_pattern_message, "includes common character sequence"
auth_value_method :password_in_dictionary_message, "is a word in a dictionary"
auth_value_methods(
:password_too_many_repeating_characters_message
)
def password_meets_requirements?(password)
super && \
password_has_enough_character_groups?(password) && \
password_has_no_invalid_pattern?(password) && \
password_not_too_many_repeating_characters?(password) && \
password_not_in_dictionary?(password)
end
def post_configure
super
return if singleton_methods.map(&:to_sym).include?(:password_dictionary)
case dictionary_file = password_dictionary_file
when false
return
when nil
default_dictionary_file = '/usr/share/dict/words'
if File.file?(default_dictionary_file)
words = File.read(default_dictionary_file)
end
else
words = File.read(password_dictionary_file)
end
return unless words
require 'set'
dict = Set.new(words.downcase.split)
self.class.send(:define_method, :password_dictionary){dict}
end
private
def password_has_enough_character_groups?(password)
return true if password.length > password_max_length_for_groups_check
return true if password_character_groups.select{|re| password =~ re}.length >= password_min_groups
@password_requirement_message = password_not_enough_character_groups_message
false
end
def password_has_no_invalid_pattern?(password)
return true unless password_invalid_pattern
return true if password !~ password_invalid_pattern
@password_requirement_message = password_invalid_pattern_message
false
end
def password_not_too_many_repeating_characters?(password)
return true if password_max_repeating_characters < 2
return true if password !~ /(.)(\1){#{password_max_repeating_characters-1}}/
@password_requirement_message = password_too_many_repeating_characters_message
false
end
def password_too_many_repeating_characters_message
"contains #{password_max_repeating_characters} or more of the same character in a row"
end
def password_not_in_dictionary?(password)
return true unless dict = password_dictionary
return true unless password =~ /\A(?:\d*)([A-Za-z!@$+|][A-Za-z!@$+|0134578]+[A-Za-z!@$+|])(?:\d*)\z/
word = $1.downcase.tr('!@$+|0134578', 'iastloleastb')
return true if !dict.include?(word)
@password_requirement_message = password_in_dictionary_message
false
end
end
- PasswordExpiration =
Feature.define(:password_expiration) do
depends :login, :change_password
error_flash "Your password has expired and needs to be changed"
error_flash "Your password cannot be changed yet", 'password_not_changeable_yet'
redirect :password_not_changeable_yet
redirect(:password_change_needed){"#{prefix}/#{change_password_route}"}
auth_value_method :allow_password_change_after, 0
auth_value_method :require_password_change_after, 90*86400
auth_value_method :password_expiration_table, :account_password_change_times
auth_value_method :password_expiration_id_column, :id
auth_value_method :password_expiration_changed_at_column, :changed_at
auth_value_method :password_changed_at_session_key, :password_changed_at
auth_value_method :password_expiration_default, false
auth_methods(
:password_expired?,
:update_password_changed_at
)
def get_password_changed_at
convert_timestamp(password_expiration_ds.get(password_expiration_changed_at_column))
end
def check_password_change_allowed
if password_changed_at = get_password_changed_at
if password_changed_at > Time.now - allow_password_change_after
set_redirect_error_flash password_not_changeable_yet_error_flash
redirect password_not_changeable_yet_redirect
end
end
end
def set_password(password)
update_password_changed_at
session[password_changed_at_session_key] = Time.now.to_i
super
end
def account_from_reset_password_key(key)
if a = super
check_password_change_allowed
end
a
end
def update_password_changed_at
ds = password_expiration_ds
if ds.update(password_expiration_changed_at_column=>Sequel::CURRENT_TIMESTAMP) == 0
ignore_uniqueness_violation{ds.insert(password_expiration_id_column=>account_id)}
end
end
def require_current_password
if authenticated? && password_expired? && password_change_needed_redirect != request.path_info
set_redirect_error_flash password_expiration_error_flash
redirect password_change_needed_redirect
end
end
def password_expired?
if password_changed_at = session[password_changed_at_session_key]
return password_changed_at + require_password_change_after < Time.now.to_i
end
account_from_session
if password_changed_at = get_password_changed_at
set_session_value(password_changed_at_session_key, password_changed_at.to_i)
password_changed_at + require_password_change_after < Time.now
else
set_session_value(password_changed_at_session_key, password_expiration_default ? 0 : 2147483647)
password_expiration_default
end
end
private
def after_close_account
super if defined?(super)
password_expiration_ds.delete
end
def before_change_password_route
check_password_change_allowed
super
end
def after_create_account
if account_password_hash_column
update_password_changed_at
end
super if defined?(super)
end
def after_login
require_current_password
super
end
def password_expiration_ds
db[password_expiration_table].where(password_expiration_id_column=>account_id)
end
end
- VerifyChangeLogin =
Feature.define(:verify_change_login) do
depends :change_login, :verify_account_grace_period
def change_login_notice_flash
"#{super}. #{verify_account_email_sent_notice_flash}"
end
private
def after_change_login
super
update_account(account_status_column=>account_unverified_status_value)
setup_account_verification
session[unverified_account_session_key] = true
end
end
- PasswordGracePeriod =
Feature.define(:password_grace_period) do
auth_value_method :password_grace_period, 300
auth_value_method :last_password_entry_session_key, :last_password_entry
def modifications_require_password?
return false unless super
!password_recently_entered?
end
def password_match?(_)
if v = super
@last_password_entry = set_last_password_entry
end
v
end
private
def after_create_account
super if defined?(super)
@last_password_entry = Time.now.to_i
end
def after_reset_password
super if defined?(super)
@last_password_entry = Time.now.to_i
end
def update_session
super
session[last_password_entry_session_key] = @last_password_entry if defined?(@last_password_entry)
end
def password_recently_entered?
return false unless last_password_entry = session[last_password_entry_session_key]
last_password_entry + password_grace_period > Time.now.to_i
end
def set_last_password_entry
session[last_password_entry_session_key] = Time.now.to_i
end
end
- DisallowPasswordReuse =
Feature.define(:disallow_password_reuse) do
depends :login_password_requirements_base
auth_value_method :password_same_as_previous_password_message, "same as previous password"
auth_value_method :previous_password_account_id_column, :account_id
auth_value_method :previous_password_hash_column, :password_hash
auth_value_method :previous_password_hash_table, :account_previous_password_hashes
auth_value_method :previous_password_id_column, :id
auth_value_method :previous_passwords_to_check, 6
auth_methods(
:add_previous_password_hash,
:password_doesnt_match_previous_password?
)
def set_password(password)
hash = super
add_previous_password_hash(hash)
hash
end
def add_previous_password_hash(hash)
ds = previous_password_ds
keep_before = ds.reverse(previous_password_id_column).
limit(nil, previous_passwords_to_check).
get(previous_password_id_column)
ds.where(Sequel.expr(previous_password_id_column) <= keep_before).
delete
ds.insert(previous_password_account_id_column=>account_id, previous_password_hash_column=>hash)
end
def password_meets_requirements?(password)
super &&
password_doesnt_match_previous_password?(password)
end
private
def password_doesnt_match_previous_password?(password)
id = account_id
match = if use_database_authentication_functions?
salts = previous_password_ds.
select_map([previous_password_id_column, Sequel.function(function_name(:rodauth_get_previous_salt), previous_password_id_column).as(:salt)])
return true if salts.empty?
salts.any? do |hash_id, salt|
db.get(Sequel.function(function_name(:rodauth_previous_password_hash_match), hash_id, BCrypt::Engine.hash_secret(password, salt)))
end
else
previous_password_ds.select_map(previous_password_hash_column).any?{|hash| BCrypt::Password.new(hash) == password}
end
return true unless match
@password_requirement_message = password_same_as_previous_password_message
false
end
def after_close_account
super if defined?(super)
previous_password_ds.delete
end
def after_create_account
if account_password_hash_column
add_previous_password_hash(password_hash(request[password_param]))
end
super if defined?(super)
end
def previous_password_ds
db[previous_password_hash_table].where(previous_password_account_id_column=>account_id)
end
end
- VerifyAccountGracePeriod =
Feature.define(:verify_account_grace_period) do
depends :verify_account
error_flash "Cannot change login for unverified account. Please verify this account before changing the login.", "unverified_change_login"
redirect :unverified_change_login
auth_value_method :verification_requested_at_column, :requested_at
auth_value_method :unverified_account_session_key, :unverified_account
auth_value_method :verify_account_grace_period, 86400
auth_methods(
:account_in_unverified_grace_period?
)
def verified_account?
logged_in? && !session[unverified_account_session_key]
end
def create_account_autologin?
true
end
def open_account?
super || account_in_unverified_grace_period?
end
private
def after_close_account
super if defined?(super)
verify_account_ds.delete
end
def before_change_login_route
unless verified_account?
set_redirect_error_flash unverified_change_login_error_flash
redirect unverified_change_login_redirect
end
super if defined?(super)
end
def verify_account_check_already_logged_in
nil
end
def account_session_status_filter
s = super
if verify_account_grace_period
grace_period_ds = db[verify_account_table].
select(verify_account_id_column).
where((Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP))
s = Sequel.|(s, Sequel.expr(account_status_column=>account_unverified_status_value) & {account_id_column => grace_period_ds})
end
s
end
def update_session
super
if account_in_unverified_grace_period?
session[unverified_account_session_key] = true
end
end
def account_in_unverified_grace_period?
account[account_status_column] == account_unverified_status_value &&
verify_account_grace_period &&
!verify_account_ds.where(Sequel.date_add(verification_requested_at_column, :seconds=>verify_account_grace_period) > Sequel::CURRENT_TIMESTAMP).empty?
end
def use_date_arithmetic?
true
end
end
- LoginPasswordRequirementsBase =
Feature.define(:login_password_requirements_base) do
auth_value_method :login_confirm_param, 'login-confirm'
auth_value_method :login_minimum_length, 3
auth_value_method :login_maximum_length, 255
auth_value_method :logins_do_not_match_message, 'logins do not match'
auth_value_method :password_confirm_param, 'password-confirm'
auth_value_method :password_minimum_length, 6
auth_value_method :passwords_do_not_match_message, 'passwords do not match'
auth_value_method :require_email_address_logins?, true
auth_value_method :require_login_confirmation?, true
auth_value_method :require_password_confirmation?, true
auth_value_method :same_as_existing_password_message, "invalid password, same as current password"
auth_value_methods(
:login_confirm_label,
:login_does_not_meet_requirements_message,
:login_too_long_message,
:login_too_short_message,
:password_confirm_label,
:password_does_not_meet_requirements_message,
:password_hash_cost,
:password_too_short_message
)
auth_methods(
:login_meets_requirements?,
:password_hash,
:password_meets_requirements?,
:set_password
)
def login_confirm_label
"Confirm #{login_label}"
end
def password_confirm_label
"Confirm #{password_label}"
end
def login_meets_requirements?(login)
login_meets_length_requirements?(login) && \
login_meets_email_requirements?(login)
end
def password_meets_requirements?(password)
password_meets_length_requirements?(password) && \
password_does_not_contain_null_byte?(password)
end
def set_password(password)
hash = password_hash(password)
if account_password_hash_column
update_account(account_password_hash_column=>hash)
elsif password_hash_ds.update(password_hash_column=>hash) == 0
db[password_hash_table].insert(password_hash_id_column=>account_id, password_hash_column=>hash)
end
hash
end
private
attr_reader :login_requirement_message
attr_reader :password_requirement_message
def password_does_not_meet_requirements_message
"invalid password, does not meet requirements#{" (#{password_requirement_message})" if password_requirement_message}"
end
def password_too_short_message
"minimum #{password_minimum_length} characters"
end
def login_does_not_meet_requirements_message
"invalid login#{", #{login_requirement_message}" if login_requirement_message}"
end
def login_too_long_message
"maximum #{login_maximum_length} characters"
end
def login_too_short_message
"minimum #{login_minimum_length} characters"
end
def login_meets_length_requirements?(login)
if login_minimum_length > login.length
@login_requirement_message = login_too_short_message
false
elsif login_maximum_length < login.length
@login_requirement_message = login_too_long_message
false
else
true
end
end
def login_meets_email_requirements?(login)
return true unless require_email_address_logins?
if login =~ /\A[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+\z/
return true
end
@login_requirement_message = 'not a valid email address'
return false
end
def password_meets_length_requirements?(password)
return true if password_minimum_length <= password.length
@password_requirement_message = password_too_short_message
false
end
def password_does_not_contain_null_byte?(password)
return true unless password.include?("\0")
@password_requirement_message = 'contains null byte'
false
end
if ENV['RACK_ENV'] == 'test'
def password_hash_cost
BCrypt::Engine::MIN_COST
end
else
def password_hash_cost
BCrypt::Engine::DEFAULT_COST
end
end
def password_hash(password)
BCrypt::Password.create(password, :cost=>password_hash_cost)
end
end