Module: Rodauth

Defined in:
lib/rodauth.rb,
lib/rodauth/version.rb,
lib/rodauth/migrations.rb,
lib/rodauth/features/jwt.rb,
lib/rodauth/features/otp.rb,
lib/rodauth/features/base.rb,
lib/rodauth/features/login.rb,
lib/rodauth/features/logout.rb,
lib/rodauth/features/lockout.rb,
lib/rodauth/features/remember.rb,
lib/rodauth/features/sms_codes.rb,
lib/rodauth/features/email_base.rb,
lib/rodauth/features/change_login.rb,
lib/rodauth/features/close_account.rb,
lib/rodauth/features/create_account.rb,
lib/rodauth/features/recovery_codes.rb,
lib/rodauth/features/reset_password.rb,
lib/rodauth/features/single_session.rb,
lib/rodauth/features/verify_account.rb,
lib/rodauth/features/change_password.rb,
lib/rodauth/features/two_factor_base.rb,
lib/rodauth/features/confirm_password.rb,
lib/rodauth/features/account_expiration.rb,
lib/rodauth/features/session_expiration.rb,
lib/rodauth/features/password_complexity.rb,
lib/rodauth/features/password_expiration.rb,
lib/rodauth/features/verify_change_login.rb,
lib/rodauth/features/password_grace_period.rb,
lib/rodauth/features/disallow_password_reuse.rb,
lib/rodauth/features/verify_account_grace_period.rb,
lib/rodauth/features/login_password_requirements_base.rb

Defined Under Namespace

Modules: ClassMethods, InstanceMethods, RequestMethods Classes: Auth, Configuration, Feature, FeatureConfiguration

Constant Summary collapse

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, header = 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.headers['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.headers['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_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|
    

    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_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 otp_auth_form_footer
    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
    []
  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)
    # Uniqueness errors can't be handled here, as we can't be sure the secret provided
    # is the same as the current 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 (model)
      warn "account_model is deprecated, use db and accounts_table settings"
      db model.db
      accounts_table model.table_name
       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 
    []
  end
  alias  

  def session_value
    session[session_key]
  end
  alias logged_in? session_value

  def ()
    @account = ()
  end

  def open_account?
    skip_status_checks? || [] ==  
  end

  def db
    Sequel::DATABASES.first
  end

  # If the account_password_hash_column is set, the password hash is verified in
  # ruby, it will not use a database function to do so, it will check the password
  # hash using bcrypt.
  def 
    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 
    set_redirect_error_flash 
    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 
     unless logged_in?
  end

  def authenticated?
    logged_in?
  end

  def require_authentication
    
  end

  def 
    
  end

  def 
    @account = 
  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 
      BCrypt::Password.new([]) == password
    elsif use_database_authentication_functions?
      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
      # :nocov:
      if hash = password_hash_ds.get(password_hash_column)
        BCrypt::Password.new(hash) == password
      end
      # :nocov:
    end
  end

  private

  def update_session
    clear_session
    session[session_key] = 
  end

  # Return a string for the parameter name.  This will be an empty
  # string if the parameter doesn't exist.
  def param(key)
    param_or_nil(key).to_s
  end

  # Return a string for the parameter name, or nil if there is no
  # parameter with that name.
  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
    # :nocov:
    def random_key
      SecureRandom.hex(32)
    end
    # :nocov:
  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_authentication
    
  end

  def 
    unless 
      clear_session
      
    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
      # :nocov:
      false
      # :nocov:
    end
  end

  def function_name(name)
    if db.database_type == :mssql
      # :nocov:
      "dbo.#{name}"
      # :nocov:
    else
      name
    end
  end

  def ()
    ds = db[accounts_table].where(=>)
    ds = ds.select(*) if 
    ds = ds.where(=>[, ]) unless skip_status_checks?
    ds.first
  end

  def 
    ds = (session_value)
    ds = ds.where() unless skip_status_checks?
    ds.first
  end

  def 
    {=>}
  end

  def template_path(page)
    File.join(File.dirname(__FILE__), '../../../templates', "#{page}.str")
  end

  def (id=)
    raise ArgumentError, "invalid account id passed to account_ds" unless id
    ds = db[accounts_table].where(=>id)
    ds = ds.select(*) if 
    ds
  end

  def password_hash_ds
    db[password_hash_table].where(password_hash_id_column=>)
  end

  # This is needed for jdbc/sqlite, which returns timestamp columns as strings
  def convert_timestamp(timestamp)
    timestamp = db.to_application_timestamp(timestamp) if timestamp.is_a?(String)
    timestamp
  end

  # This is used to avoid race conditions when using the pattern of inserting when
  # an update affects no rows.  In such cases, if a row is inserted between the
  # update and the insert, the insert will fail with a uniqueness error, but
  # retrying will work.  It is possible for it to fail again, but only if the row
  # is deleted before the update and readded before the insert, which is very
  # unlikely to happen.  In such cases, raising an exception is acceptable.
  def retry_on_uniqueness_violation(&block)
    if raises_uniqueness_violation?(&block)
      yield
    end
  end

  # In cases where retrying on uniqueness violations cannot work, this will detect
  # whether a uniqueness violation is raised by the block and return the exception if so.
  # This method should be used if you don't care about the exception itself.
  def raises_uniqueness_violation?(&block)
    transaction(:savepoint=>:only, &block)
    false
  rescue unique_constraint_violation_class => e
    e
  end

  # Work around jdbc/sqlite issue where it only raises ConstraintViolation and not
  # UniqueConstraintViolation.
  def unique_constraint_violation_class
    if db.adapter_scheme == :jdbc && db.database_type == :sqlite
      # :nocov:
      Sequel::ConstraintViolation
      # :nocov:
    else
      Sequel::UniqueConstraintViolation
    end
  end

  # If you would like to operate/reraise the exception, this alias makes more sense.
  alias raised_uniqueness_violation raises_uniqueness_violation?

  # If you just want to ignore uniqueness violations, this alias makes more sense.
  alias ignore_uniqueness_violation raises_uniqueness_violation?

  # This is needed on MySQL, which doesn't support non constant defaults other than
  # CURRENT_TIMESTAMP.
  def set_deadline_value(hash, column, interval)
    if set_deadline_values?
      # :nocov:
      hash[column] = Sequel.date_add(Sequel::CURRENT_TIMESTAMP, interval)
      # :nocov:
    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|
        [k] = v == Sequel::CURRENT_TIMESTAMP ? Time.now : v
      end
    end
    num
  end

  def (values, ds=)
    update_hash_ds(, 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
    

    r.get do
      
    end

    r.post do
      clear_session

      catch_error do
        unless (param())
          throw_error(, )
        end

        

        unless open_account?
          throw_error(, )
        end

        unless password_match?(param(password_param))
          
          throw_error(password_param, invalid_password_message)
        end

        transaction do
          
          update_session
          
        end
        set_notice_flash 
        redirect 
      end

      set_error_flash 
      
    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{}

  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
    

    r.post do
      if (param()) && 
        transaction do
          
          
          
        end

        set_notice_flash 
      else
        set_redirect_error_flash 
      end

      redirect 
    end
  end

  route(:unlock_account) do |r|
    check_already_logged_in
    

    r.get do
      if (param())
        
      else
        set_redirect_error_flash 
        redirect 
      end
    end

    r.post do
      key = param()
      unless (key)
        set_redirect_error_flash 
        redirect 
      end

      if ! || password_match?(param(password_param))
        transaction do
          
          
          
          if 
            update_session
          end
        end

        set_notice_flash 
        redirect 
      else
        set_field_error(password_param, invalid_password_message)
        set_error_flash 
        
      end
    end
  end

  def locked_out?
    if t = convert_timestamp(.get())
      if Time.now < t
        true
      else
        
        false
      end
    else
      false
    end
  end

  def 
    transaction do
      
    end
  end

  def 
    
  end

  def 
    ds = .
        where(=>)

    number = if db.database_type == :postgres
      ds.returning().
        with_sql(:update_sql, =>Sequel.expr()+1).
        single_value
    else
      # :nocov:
      if ds.update(=>Sequel.expr()+1) > 0
        ds.get()
      end
      # :nocov:
    end

    unless number
      # Ignoring the violation is safe here.  It may allow slightly more than max_invalid_logins invalid logins before
      # lockout, but allowing a few extra is OK if the race is lost.
      ignore_uniqueness_violation{.insert(=>)}
      number = 1
    end

    if number >= max_invalid_logins
      @unlock_account_key_value = 
      hash = {=>, =>}
      set_deadline_value(hash, , )

      if e = raised_uniqueness_violation{.insert(hash)}
        # If inserting into the lockout table raises a violation, we should just be able to pull the already inserted
        # key out of it.  If that doesn't return a valid key, we should reraise the error.
        raise e unless @unlock_account_key_value = .get()

        show_lockout_page
      end
    end
  end

  def 
    .get()
  end

  def (key)
    @account = (key)
  end

  def 
    @unlock_account_key_value = 
    .deliver!
  end

  def 
    token_link(, , )
  end

  private

  attr_reader :unlock_account_key_value

  def 
    if locked_out?
      show_lockout_page
    end
    super
  end

  def 
    
    super
  end

  def 
    
    super
  end

  def 
    
    super if defined?(super)
  end

  def 
    random_key
  end

  def 
    .delete
    .delete
  end

  def show_lockout_page
    set_error_flash 
    response.write 
    request.halt
  end

  def 
    create_email(, )
  end

  def 
    render('unlock-account-email')
  end

  def use_date_arithmetic?
    db.database_type == :mysql
  end

  def 
    db[].where(=>)
  end

  def (id=)
    db[].where(=>id)
  end

  def (token)
    (token){|id| (id).get()}
  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|
    
    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
            
          when remember_forget_param_value
             
          when remember_disable_param_value
             
          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)
      
      return
    end

    session[session_key] = id
     = 
    session.delete(session_key)

    unless 
      remove_remember_key(id)
      
      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))
      
    end
    after_load_memory
  end

  def 
    get_remember_key
    opts = Hash[remember_cookie_options]
    opts[:value] = "#{}_#{remember_key_value}"
    opts[:expires] = convert_timestamp(active_remember_key_ds.get(remember_deadline_column))
    ::Rack::Utils.set_cookie_header!(response.headers, remember_cookie_key, opts)
  end

  def 
    ::Rack::Utils.delete_cookie_header!(response.headers, 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 
    remove_remember_key
  end

  def add_remember_key
    hash = {remember_id_column=>, 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)}
      # If inserting into the remember key table causes a violation, we can pull the 
      # existing row from the table.  If there is no invalid row, we can then reraise.
      raise e unless @remember_key_value = active_remember_key_ds.get(remember_key_column)
    end
  end

  def remove_remember_key(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
    
    super if defined?(super)
  end

  def 
    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=)
    db[remember_table].where(remember_id_column=>id)
  end

  def active_remember_key_ds(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_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_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|
    
    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|
    
    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_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 otp_auth_form_footer
    "#{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)
    # Cannot handle uniqueness violation here, as the phone number given may not match the
    # one in the table.
    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
    []
  end

  def split_token(token)
    token.split(token_separator, 2)
  end

  def token_link(route, param, key)
    "#{request.base_url}#{prefix}/#{route}?#{param}=#{}#{token_separator}#{key}"
  end

  def (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 = (id)
    ds = ds.where(=>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|
    
    

    r.get do
      
    end

    r.post do
      catch_error do
        if  && !password_match?(param(password_param))
          throw_error(password_param, invalid_password_message)
        end

         = param()
        unless ()
          throw_error(, )
        end

        if  &&  != param()
          throw_error(, logins_do_not_match_message)
        end

        transaction do
          
          unless ()
            throw_error(, )
          end

          
          set_notice_flash 
          redirect 
        end
      end

      set_error_flash 
      
    end
  end

  def 
    modifications_require_password?
  end

  def ()
    updated = nil
    if .get().downcase == .downcase
      @login_requirement_message = 'same as current login'
      return false
    end
    raised = raises_uniqueness_violation?{updated = ({=>}, .exclude(=>)) == 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|
    
    

    r.get do
      
    end

    r.post do
      if ! || password_match?(param(password_param))
        transaction do
          
          
          
          if 
            
          end
        end
        clear_session

        set_notice_flash 
        redirect 
      else
        set_field_error(password_param, invalid_password_message)
        set_error_flash 
        
      end
    end
  end

  def 
    modifications_require_password?
  end

  def 
    unless skip_status_checks?
      (=>)
    end

    unless 
      password_hash_ds.delete
    end
  end

  def 
    .delete
  end

  def 
    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
    

    r.get do
      
    end

    r.post do
       = param()
      password = param(password_param)
      ()

      if 
        (param(password_param))
      end

      catch_error do
        if  &&  != param()
          throw_error(, logins_do_not_match_message)
        end

        unless ()
          throw_error(, )
        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
          
          unless 
            throw_error(, )
          end

          unless 
            set_password(password)
          end
          
          if 
            update_session
          end
          set_notice_flash 
          redirect 
        end
      end

      set_error_flash 
      
    end
  end

  def 
    "<p><a href=\"#{prefix}/#{}\">Create a New Account</a></p>"
  end

  def 
    super + 
  end

  def (password)
    [] = password_hash(password)
  end

  def ()
    @account = ()
  end
  
  def 
    id = nil
    raised = raises_uniqueness_violation?{id = db[accounts_table].insert()}

    if raised
      @login_requirement_message = 'already an account with this login'
    end

    if id
      [] = id
    end

    id && !raised
  end

  private

  def ()
    acc = {=>}
    unless skip_status_checks?
      acc[] = 
    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_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|
    
    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 otp_auth_form_footer
    "#{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
    # This should never raise uniqueness violations unless the recovery code is the same, and the odds of that
    # are 1/256**32 assuming a good random number generator.  Still, attempt to handle that case by retrying
    # on such a uniqueness violation.
    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 (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 (key)
          reset_password_view
        else
          set_redirect_error_flash no_matching_reset_password_key_message
          redirect 
        end
      end
    end

    r.post do
      key = param(reset_password_key_param)
      unless (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)}
          # If inserting into the reset password table causes a violation, we can pull the 
          # existing reset password key from the table, or reraise.
          raise e unless @reset_password_key_value = get_password_reset_key()
        end
      end
    end
  end

  def remove_reset_password_key
    password_reset_ds.delete
  end

  def (key)
    @account = (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 
    @login_form_header = render("reset-password-request")
    super
  end

  def 
    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=>, 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=)
    db[reset_password_table].where(reset_password_id_column=>id)
  end

  def (token)
    (token, ){|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
        # No row exists for this user, indicating the feature has never
        # been used, so it is OK to treat the current session as a new
        # session.
        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
      # Don't handle uniqueness violations here.  While we could get the stored key from the
      # database, it could lead to two sessions sharing the same key, which this feature is
      # designed to prevent.
      single_session_ds.insert(single_session_id_column=>session_value, single_session_key_column=>key)
    end
  end

  private

  def 
    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){}

  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|
    
    

    r.post do
      if (param()) && !open_account?
        
        if 
          
        end

        set_notice_flash 
      else
        set_redirect_error_flash 
      end
      
      redirect 
    end
  end

  route do |r|
    
    

    r.get do
      if key = param_or_nil()
        if (key)
          
        else
          set_redirect_error_flash 
          redirect 
        end
      end
    end

    r.post do
      key = param()
      unless (key)
        set_redirect_error_flash 
        redirect 
      end

      transaction do
        
        
        
        
      end

      if 
        update_session
      end

      set_notice_flash 
      redirect 
    end
  end

  def 
    .delete
  end

  def 
    (=>) == 1
  end

  def 
    if @verify_account_key_value = ()
      
      true
    end
  end

  def 
    
  end

  def ()
    if ()
      set_error_flash 
      response.write 
      request.halt
    end
    super
  end

  def (key)
    @account = (key)
  end

  def 
    
  end

  def 
    .deliver!
  end

  def 
    token_link(, , )
  end

  def (id)
    (id).get()
  end

  def skip_status_checks?
    false
  end

  def 
    false
  end

  private

  attr_reader :verify_account_key_value

  def 
    unless open_account?
      set_error_flash 
      response.write 
      request.halt
    end
    super
  end

  def 
    
    super
  end

  def 
    
    
    
  end

  def 
    check_already_logged_in
  end

  def 
    @verify_account_key_value = random_key
  end

  def 
    ds = 
    transaction do
      if ds.empty?
        if e = raised_uniqueness_violation{ds.insert()}
          # If inserting into the verify account table causes a violation, we can pull the 
          # key from the verify account table, or reraise.
          raise e unless @verify_account_key_value = ()
        end
      end
    end
  end

  def 
    {=>, =>}
  end

  def 
    create_email(, )
  end

  def 
    render('verify-account-email')
  end

  def (id=)
    db[].where(=>id)
  end

  def (token)
    (token, ){|id| (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|
    
    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 
    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
    
    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 
    get_activity_timestamp(session_value, )
  end

  def 
    get_activity_timestamp(session_value, )
  end

  def 
    get_activity_timestamp(, )
  end

  def 
    update_activity(, , )
  end

  def update_last_activity
    if session_value
      update_activity(session_value, )
    end
  end

  def set_expired
    update_activity(, )
    
  end

  def 
    columns = [, , ]
    last_activity, , expired = ().get(columns)
    return true if expired
    timestamp = convert_timestamp( ? last_activity : )
    return false unless timestamp
    timestamp < Time.now - 
  end

  def 
    if 
      set_expired unless 
      set_redirect_error_flash 
      redirect 
    end
    
  end

  private

  def 
    super if defined?(super)
    ().delete
  end

  def update_session
    
    super
  end

  def ()
    db[].
      where(=>)
  end

  def get_activity_timestamp(, column)
    convert_timestamp(().get(column))
  end

  def update_activity(, *columns)
    ds = ()
    hash = {}
    columns.each do |c|
      hash[c] = Sequel::CURRENT_TIMESTAMP
    end
    if ds.update(hash) == 0
      hash[] = 
      hash[] ||= Sequel::CURRENT_TIMESTAMP
      hash[] ||= Sequel::CURRENT_TIMESTAMP
      # It is safe to ignore uniqueness violations here, as a concurrent insert would also use current timestamps.
      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
    
  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 (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
      # Ignoring the violation is safe here, since a concurrent insert would also set it to the
      # current timestamp.
      ignore_uniqueness_violation{ds.insert(password_expiration_id_column=>)}
    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

    
    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 
    super if defined?(super)
    password_expiration_ds.delete
  end

  def before_change_password_route
    check_password_change_allowed
    super
  end

  def 
    if 
      update_password_changed_at
    end
    super if defined?(super)
  end

  def 
    require_current_password
    super
  end

  def password_expiration_ds
    db[password_expiration_table].where(password_expiration_id_column=>)
  end
end
VerifyChangeLogin =
Feature.define(:verify_change_login) do
  depends :change_login, :verify_account_grace_period

  def 
    "#{super}. #{}"
  end

  private

  def 
    super
    (=>)
    
    session[] = 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 
    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

    # This should never raise uniqueness violations, as it uses a serial primary key
    ds.insert(=>, 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 = 
    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
      # :nocov:
      previous_password_ds.select_map(previous_password_hash_column).any?{|hash| BCrypt::Password.new(hash) == password}
      # :nocov:
    end

    return true unless match
    @password_requirement_message = password_same_as_previous_password_message
    false
  end

  def 
    super if defined?(super)
    previous_password_ds.delete
  end

  def 
    if 
      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(=>)
  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[]
  end

  def 
    true
  end

  def open_account?
    super || 
  end

  private

  def 
    super if defined?(super)
    .delete
  end
  
  def 
    unless verified_account?
      set_redirect_error_flash 
      redirect 
    end
    super if defined?(super)
  end

  def 
    nil
  end

  def 
    s = super
    if 
      grace_period_ds = db[].
        select().
        where((Sequel.date_add(verification_requested_at_column, :seconds=>) > Sequel::CURRENT_TIMESTAMP))
      s = Sequel.|(s, Sequel.expr(=>) & { => grace_period_ds})
    end
    s
  end

  def update_session
    super
    if 
      session[] = true
    end
  end

  def 
    [] ==  &&
       &&
      !.where(Sequel.date_add(verification_requested_at_column, :seconds=>) > 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 
    "Confirm #{}"
  end

  def password_confirm_label
    "Confirm #{password_label}"
  end

  def ()
    () && \
      ()
  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 
      (=>hash)
    elsif password_hash_ds.update(password_hash_column=>hash) == 0
      # This shouldn't raise a uniqueness error, as the update should only fail for a new user,
      # and an existing user shouldn't always havae a valid password hash row.  If this does
      # fail, retrying it will cause problems, it will override a concurrently running update
      # with potentially a different password.
      db[password_hash_table].insert(password_hash_id_column=>, 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 
    "invalid login#{", #{}" if }"
  end

  def 
    "maximum #{} characters"
  end

  def 
    "minimum #{} characters"
  end

  def ()
    if  > .length
      @login_requirement_message = 
      false
    elsif  < .length
      @login_requirement_message = 
      false
    else
      true
    end
  end

  def ()
    return true unless require_email_address_logins?
    if  =~ /\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
    # :nocov:
    def password_hash_cost
      BCrypt::Engine::DEFAULT_COST
    end
    # :nocov:
  end

  def password_hash(password)
    BCrypt::Password.create(password, :cost=>password_hash_cost)
  end
end

Class Method Summary collapse

Class Method Details

.configure(app, opts = {}, &block) ⇒ Object



21
22
23
# File 'lib/rodauth.rb', line 21

def self.configure(app, opts={}, &block)
  ((app.opts[:rodauths] ||= {})[opts[:name]] ||= Class.new(Auth)).configure(&block)
end

.create_database_authentication_functions(db, opts = {}) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/rodauth/migrations.rb', line 4

def self.create_database_authentication_functions(db, opts={})
  table_name = opts[:table_name] || :account_password_hashes
  get_salt_name = opts[:get_salt_name] || :rodauth_get_salt
  valid_hash_name = opts[:valid_hash_name] || :rodauth_valid_password_hash 
  case db.database_type
  when :postgres
    db.run <<END
CREATE OR REPLACE FUNCTION #{get_salt_name}(acct_id int8) RETURNS text AS $$
DECLARE salt text;
BEGIN
SELECT substr(password_hash, 0, 30) INTO salt 
FROM #{table_name}
WHERE acct_id = id;
RETURN salt;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
END

    db.run <<END
CREATE OR REPLACE FUNCTION #{valid_hash_name}(acct_id int8, hash text) RETURNS boolean AS $$
DECLARE valid boolean;
BEGIN
SELECT password_hash = hash INTO valid 
FROM #{table_name}
WHERE acct_id = id;
RETURN valid;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, pg_temp;
END
  when :mysql
    db.run <<END
CREATE FUNCTION #{get_salt_name}(acct_id int8) RETURNS varchar(255)
SQL SECURITY DEFINER
READS SQL DATA
BEGIN
DECLARE salt varchar(255);
DECLARE csr CURSOR FOR
SELECT substr(password_hash, 1, 30)
FROM #{table_name}
WHERE acct_id = id;
OPEN csr;
FETCH csr INTO salt;
CLOSE csr;
RETURN salt;
END;
END

    db.run <<END
CREATE FUNCTION #{valid_hash_name}(acct_id int8, hash varchar(255)) RETURNS tinyint(1)
SQL SECURITY DEFINER
READS SQL DATA
BEGIN
DECLARE valid tinyint(1);
DECLARE csr CURSOR FOR 
SELECT password_hash = hash
FROM #{table_name}
WHERE acct_id = id;
OPEN csr;
FETCH csr INTO valid;
CLOSE csr;
RETURN valid;
END;
END
  when :mssql
    db.run <<END
CREATE FUNCTION #{get_salt_name}(@account_id bigint) RETURNS nvarchar(255)
WITH EXECUTE AS OWNER
AS
BEGIN
DECLARE @salt nvarchar(255);
SELECT @salt = substring(password_hash, 0, 30)
FROM #{table_name}
WHERE id = @account_id;
RETURN @salt;
END;
END

    db.run <<END
CREATE FUNCTION #{valid_hash_name}(@account_id bigint, @hash nvarchar(255)) RETURNS bit
WITH EXECUTE AS OWNER
AS
BEGIN
DECLARE @valid bit;
DECLARE @ph nvarchar(255);
SELECT @ph = password_hash
FROM #{table_name}
WHERE id = @account_id;
IF(@hash = @ph)
SET @valid = 1;
ELSE
SET @valid = 0
RETURN @valid;
END;
END
  end
end

.create_database_previous_password_check_functions(db) ⇒ Object



116
117
118
# File 'lib/rodauth/migrations.rb', line 116

def self.create_database_previous_password_check_functions(db)
  create_database_authentication_functions(db, :table_name=>:account_previous_password_hashes, :get_salt_name=>:rodauth_get_previous_salt, :valid_hash_name=>:rodauth_previous_password_hash_match)
end

.drop_database_authentication_functions(db) ⇒ Object



105
106
107
108
109
110
111
112
113
114
# File 'lib/rodauth/migrations.rb', line 105

def self.drop_database_authentication_functions(db)
  case db.database_type
  when :postgres
    db.run "DROP FUNCTION rodauth_get_salt(int8)"
    db.run "DROP FUNCTION rodauth_valid_password_hash(int8, text)"
  when :mysql, :mssql
    db.run "DROP FUNCTION rodauth_get_salt"
    db.run "DROP FUNCTION rodauth_valid_password_hash"
  end
end

.drop_database_previous_password_check_functions(db) ⇒ Object



120
121
122
123
124
125
126
127
128
129
# File 'lib/rodauth/migrations.rb', line 120

def self.drop_database_previous_password_check_functions(db)
  case db.database_type
  when :postgres
    db.run "DROP FUNCTION rodauth_get_previous_salt(int8)"
    db.run "DROP FUNCTION rodauth_previous_password_hash_match(int8, text)"
  when :mysql, :mssql
    db.run "DROP FUNCTION rodauth_get_previous_salt"
    db.run "DROP FUNCTION rodauth_previous_password_hash_match"
  end
end

.load_dependencies(app, opts = {}) ⇒ Object



6
7
8
9
10
11
12
13
14
15
16
17
18
19
# File 'lib/rodauth.rb', line 6

def self.load_dependencies(app, opts={})
  if opts[:json]
    app.plugin :json
    app.plugin :json_parser
  end

  unless opts[:json] == :only
    require 'tilt/string'
    app.plugin :render
    app.plugin :csrf unless opts[:csrf] == false
    app.plugin :flash unless opts[:flash] == false
    app.plugin :h
  end
end

.versionObject



6
7
8
# File 'lib/rodauth/version.rb', line 6

def self.version
  VERSION
end