# frozen-string-literal: true module Rodauth Feature.define(:email_auth, :EmailAuth) do depends :login, :email_base notice_flash "An email has been sent to you with a link to login to your account", 'email_auth_email_sent' error_flash "There was an error logging you in" error_flash "There was an error requesting an email link to authenticate", 'email_auth_request' error_flash "An email has recently been sent to you with a link to login", 'email_auth_email_recently_sent' error_flash "There was an error logging you in: invalid email authentication key", 'no_matching_email_auth_key' loaded_templates %w'email-auth email-auth-request-form email-auth-email' view 'email-auth', 'Login' additional_form_tags additional_form_tags 'email_auth_request' before 'email_auth_request' after 'email_auth_request' button 'Send Login Link Via Email', 'email_auth_request' redirect(:email_auth_email_sent){default_post_email_redirect} redirect(:email_auth_email_recently_sent){default_post_email_redirect} email :email_auth, 'Login Link' auth_value_method :email_auth_deadline_column, :deadline auth_value_method :email_auth_deadline_interval, {:days=>1}.freeze auth_value_method :email_auth_id_column, :id auth_value_method :email_auth_key_column, :key auth_value_method :email_auth_key_param, 'key' auth_value_method :email_auth_email_last_sent_column, :email_last_sent auth_value_method :email_auth_skip_resend_email_within, 300 auth_value_method :email_auth_table, :account_email_auth_keys auth_value_method :force_email_auth?, false session_key :email_auth_session_key, :email_auth_key auth_methods( :create_email_auth_key, :email_auth_email_link, :email_auth_key_insert_hash, :email_auth_key_value, :email_auth_request_form, :get_email_auth_key, :get_email_auth_email_last_sent, :remove_email_auth_key, :set_email_auth_email_last_sent ) auth_private_methods :account_from_email_auth_key internal_request_method internal_request_method :email_auth_request internal_request_method :valid_email_auth? route(:email_auth_request) do |r| check_already_logged_in before_email_auth_request_route r.post do if account_from_login(param(login_param)) && open_account? _email_auth_request else set_redirect_error_status(no_matching_login_error_status) set_error_reason :no_matching_login set_redirect_error_flash email_auth_request_error_flash end redirect email_auth_email_sent_redirect end end route do |r| check_already_logged_in before_email_auth_route r.get do if key = param_or_nil(email_auth_key_param) set_session_value(email_auth_session_key, key) redirect(r.path) end if key = session[email_auth_session_key] if account_from_email_auth_key(key) email_auth_view else remove_session_value(email_auth_session_key) set_redirect_error_flash no_matching_email_auth_key_error_flash redirect require_login_redirect end end end r.post do key = session[email_auth_session_key] || param(email_auth_key_param) unless account_from_email_auth_key(key) set_redirect_error_status(invalid_key_error_status) set_error_reason :invalid_email_auth_key set_redirect_error_flash email_auth_error_flash redirect email_auth_email_sent_redirect end login('email_auth') end end def create_email_auth_key transaction do if email_auth_key_value = get_email_auth_key(account_id) set_email_auth_email_last_sent @email_auth_key_value = email_auth_key_value elsif e = raised_uniqueness_violation{email_auth_ds.insert(email_auth_key_insert_hash)} # If inserting into the email auth table causes a violation, we can pull the # existing email auth key from the table, or reraise. raise e unless @email_auth_key_value = get_email_auth_key(account_id) end end end def set_email_auth_email_last_sent email_auth_ds.update(email_auth_email_last_sent_column=>Sequel::CURRENT_TIMESTAMP) if email_auth_email_last_sent_column end def get_email_auth_email_last_sent if column = email_auth_email_last_sent_column if ts = email_auth_ds.get(column) convert_timestamp(ts) end end end def remove_email_auth_key email_auth_ds.delete end def account_from_email_auth_key(key) @account = _account_from_email_auth_key(key) end def email_auth_email_link token_link(email_auth_route, email_auth_key_param, email_auth_key_value) end def get_email_auth_key(id) ds = email_auth_ds(id) ds.where(Sequel::CURRENT_TIMESTAMP > email_auth_deadline_column).delete ds.get(email_auth_key_column) end def email_auth_request_form render('email-auth-request-form') end def after_login_entered_during_multi_phase_login # If forcing email auth, just send the email link. _email_auth_request_and_redirect if force_email_auth? super end def use_multi_phase_login? true end def possible_authentication_methods methods = super methods << 'email_auth' if !methods.include?('password') && allow_email_auth? methods end private def _multi_phase_login_forms forms = super forms << [30, email_auth_request_form, :_email_auth_request_and_redirect] if valid_login_entered? && allow_email_auth? forms end def email_auth_email_recently_sent? (email_last_sent = get_email_auth_email_last_sent) && (Time.now - email_last_sent < email_auth_skip_resend_email_within) end def _email_auth_request_and_redirect _email_auth_request redirect email_auth_email_sent_redirect end def _email_auth_request if email_auth_email_recently_sent? set_redirect_error_flash email_auth_email_recently_sent_error_flash redirect email_auth_email_recently_sent_redirect end generate_email_auth_key_value transaction do before_email_auth_request create_email_auth_key send_email_auth_email after_email_auth_request end set_notice_flash email_auth_email_sent_notice_flash end attr_reader :email_auth_key_value def allow_email_auth? defined?(super) ? super : true end def after_login # Remove the email auth key after any login, even if # it is a password login. This is done to invalidate # the email login when a user has a password and requests # email authentication, but then remembers their password # and doesn't need the link. At that point, the link # that allows login access to the account becomes a # security liability, and it is best to remove it. remove_email_auth_key super end def after_close_account remove_email_auth_key super if defined?(super) end def generate_email_auth_key_value @email_auth_key_value = random_key end def use_date_arithmetic? super || db.database_type == :mysql end def email_auth_key_insert_hash hash = {email_auth_id_column=>account_id, email_auth_key_column=>email_auth_key_value} set_deadline_value(hash, email_auth_deadline_column, email_auth_deadline_interval) hash end def email_auth_ds(id=account_id) db[email_auth_table].where(email_auth_id_column=>id) end def _account_from_email_auth_key(token) account_from_key(token, account_open_status_value){|id| get_email_auth_key(id)} end end end