Skip to content

Instantly share code, notes, and snippets.

@jeremyevans
Last active June 24, 2021 00:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremyevans/dabf95f367109428686efa1c06b59071 to your computer and use it in GitHub Desktop.
Save jeremyevans/dabf95f367109428686efa1c06b59071 to your computer and use it in GitHub Desktop.
From 0bc589efb09312e67cd84c600fdd5e7540c04d11 Mon Sep 17 00:00:00 2001
From: Jeremy Evans <code@jeremyevans.net>
Date: Tue, 22 Jun 2021 11:27:19 -0700
Subject: [PATCH] WIP on internal requests
---
README.rdoc | 2 +
doc/internal_request.rdoc | 144 ++++++++++
lib/rodauth.rb | 64 ++++-
lib/rodauth/features/base.rb | 4 +
lib/rodauth/features/change_login.rb | 2 +
lib/rodauth/features/change_password.rb | 2 +
lib/rodauth/features/close_account.rb | 2 +
lib/rodauth/features/create_account.rb | 2 +
lib/rodauth/features/email_auth.rb | 4 +
lib/rodauth/features/internal_request.rb | 287 ++++++++++++++++++++
lib/rodauth/features/lockout.rb | 13 +-
lib/rodauth/features/login.rb | 3 +
lib/rodauth/features/otp.rb | 6 +
lib/rodauth/features/recovery_codes.rb | 4 +
lib/rodauth/features/remember.rb | 12 +-
lib/rodauth/features/reset_password.rb | 3 +
lib/rodauth/features/sms_codes.rb | 7 +
lib/rodauth/features/two_factor_base.rb | 2 +
lib/rodauth/features/verify_account.rb | 3 +
lib/rodauth/features/verify_login_change.rb | 2 +
spec/change_login_spec.rb | 27 ++
spec/change_password_spec.rb | 37 +++
spec/close_account_spec.rb | 25 ++
spec/create_account_spec.rb | 24 ++
spec/email_auth_spec.rb | 49 ++++
spec/lockout_spec.rb | 83 ++++++
spec/login_spec.rb | 35 +++
spec/remember_spec.rb | 58 ++++
spec/reset_password_spec.rb | 78 ++++++
spec/rodauth_spec.rb | 133 +++++++++
spec/spec_helper.rb | 2 +-
spec/two_factor_spec.rb | 244 +++++++++++++++++
spec/verify_account_spec.rb | 92 +++++++
spec/verify_login_change_spec.rb | 49 ++++
www/pages/documentation.erb | 1 +
www/pages/why.erb | 1 +
36 files changed, 1490 insertions(+), 16 deletions(-)
create mode 100644 doc/internal_request.rdoc
create mode 100644 lib/rodauth/features/internal_request.rb
diff --git a/README.rdoc b/README.rdoc
index 3d7a95b..39bcd72 100644
--- a/README.rdoc
+++ b/README.rdoc
@@ -60,6 +60,7 @@ HTML and JSON API for all supported features.
* Argon2
* HTTP Basic Auth
* Change Password Notify
+* Internal Request
== Resources
@@ -879,6 +880,7 @@ view the appropriate file in the doc directory.
* {Disallow Password Reuse}[rdoc-ref:doc/disallow_password_reuse.rdoc]
* {Email Authentication}[rdoc-ref:doc/email_auth.rdoc]
* {HTTP Basic Auth}[rdoc-ref:doc/http_basic_auth.rdoc]
+* {Internal Request}[rdoc-ref:doc/internal_request.rdoc]
* {JSON}[rdoc-ref:doc/json.rdoc]
* {JWT CORS}[rdoc-ref:doc/jwt_cors.rdoc]
* {JWT Refresh}[rdoc-ref:doc/jwt_refresh.rdoc]
diff --git a/doc/internal_request.rdoc b/doc/internal_request.rdoc
new file mode 100644
index 0000000..f9444d4
--- /dev/null
+++ b/doc/internal_request.rdoc
@@ -0,0 +1,144 @@
+= Documentation for Internal Request Feature
+
+The internal request feature allows interacting with Rodauth by
+calling methods, and is expected to be used mostly for administrative
+purposes. It allows for things like an changing a login or password
+for an existing user, without requiring that the user login to the
+system. The reason the feature is named +internal_request+ is that
+it internally submits requests to Rodauth, which are handled almost
+identically to how actual web requests will be handled by Rodauth.
+
+The general form of calling these methods is:
+
+ App.rodauth.internal_request_method(hash)
+
+Where +App+ is the Roda class, and +internal_request_method+ is the
+method you are calling. For example:
+
+ App.rodauth.change_password(account_id: 1, password: 'foobar')
+
+Will change the password for the account with id 1 to +foobar+.
+
+All internal request methods support the following options. For
+internal requests that require an existing account, you should
+generally use one of the two following options:
+
+:account_id :: The id of the account to be considered as logged in when the internal request is submitted (most internal requests require a login). This value is assumed to an existing account, and is not checked.
+:account_login :: The login of the account to be considered as logged in when the internal request is submitted (most internal requests require a login). This will query the database to determine the account's id before submitting the request. If there is no non-closed account for the login, this will raise an exception.
+
+There are additional options available, that you should only use
+if you have special requirements:
+
+:authenticated_by :: The array of strings to use for how the internal request's session was authenticated.
+:env :: A hash for the environment to use.
+:session :: A hash for the session to use.
+
+All remaining options are considered parameters. Using the
+previous example:
+
+ App.rodauth.change_password(account_id: 1, password: 'foobar')
+
+The <tt>password: 'foobar'</tt> part means that the parameters
+for the request will be <tt>{rodauth.password_param => 'foobar'}</tt>,
+where +rodauth.password_param+ is the value of +password_param+ in
+your Rodauth configuration (this defaults to <tt>"password"</tt>).
+
+Passing any options that are not valid Rodauth parameters will
+result in a warning.
+
+== Configuration
+
+In general, the configuration for internal requests is almost
+the same as for regular requests. There are some minor changes
+for easier usability. +modifications_require_password?+,
++require_login_confirmation?+, and +require_password_confirmation?+
+are set to false. In general, the caller of the method should not
+be able to determine the user's password, and there is no point
+in requiring parameter confirmation when calling the method
+directly.
+
+You can override the configuration for internal requests by using
+the +internal_request_configuration+ configuration method. For
+example, you can set the minimum length for logins to be 15
+for normal requests, but only 3 for internal requests:
+
+ plugin :rodauth do
+ enable :create_account, :internal_request
+ login_minimum_length 15
+
+ internal_request_configuration do
+ login_minimum_length 3
+ end
+ end
+
+Another approach for doing this is to call the +internal_request?+
+method inside configuration method blocks:
+
+ plugin :rodauth do
+ enable :create_account, :internal_request
+ login_minimum_length{internal_request? ? 3 : 15}
+ end
+
+== Return Values and Exceptions
+
+Internal request methods ending in a question mark return true or false.
+
+Most other internal request methods return nil on success, and or raise a
+Rodauth::InternalRequestError exception on failure. The exception
+message will be the {the reason for the failure}[rdoc-ref:doc/error_reasons.rdoc],
+as a string.
+
+If an internal request method returns a non-nil value on success,
+it will be documented in the section below.
+
+== Features
+
+This section documents the methods that are available for each
+feature. You must load that feature and the internal request
+feature in order to call the internal request methods for that
+feature. Some features support multiple internal request
+methods, and each internal request method supported will be
+documented under the appropriate subheading. If the method
+subheading states it it requires an account, you must pass
+the +:account_id+ or +account_login+ option when calling
+the method.
+
+=== Change Login
+
+==== change_login (requires account)
+
+The +change_login+ method changes the login for the account.
+
+The +:login+ option should be specified for the new login.
+Note that if the +:account_login+ option is specified, that
+is the current login for the account, not the new login.
+
+=== Change Password
+
+==== change_password (requires account)
+
+The +change_password+ method changes the password for the
+account.
+
+The +:password+ or +:new_password+ option should be
+specified for the new password.
+
+=== Close Account
+
+==== close_account (requires account)
+
+The +close_account+ method closes the account. There
+is no method in Rodauth to reopen closed accounts.
+
+=== Create Account
+=== Email Auth
+=== Lockout
+=== Login
+=== OTP
+=== Recovery Codes
+=== Remember
+=== Reset Password
+=== SMS Codes
+=== Two Factor Base
+=== Verify Account
+=== Verify Login Change
diff --git a/lib/rodauth.rb b/lib/rodauth.rb
index cf59ae1..1cce36d 100644
--- a/lib/rodauth.rb
+++ b/lib/rodauth.rb
@@ -39,11 +39,11 @@ module Rodauth
else
json_opt != :only
end
- auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= opts[:auth_class] || Class.new(Auth)
+ auth_class = (app.opts[:rodauths] ||= {})[opts[:name]] ||= opts[:auth_class] || Class.new(Auth){@configuration_name = opts[:name]}
if !auth_class.roda_class
auth_class.roda_class = app
elsif auth_class.roda_class != app
- auth_class = app.opts[:rodauths][opts[:name]] = Class.new(auth_class)
+ auth_class = app.opts[:rodauths][opts[:name]] = Class.new(auth_class){@configuration_name = opts[:name]}
auth_class.roda_class = app
end
auth_class.configure(&block) if block
@@ -107,6 +107,7 @@ module Rodauth
attr_accessor :dependencies
attr_accessor :routes
attr_accessor :configuration
+ attr_accessor :internal_request_methods
def route(name=feature_name, default=name.to_s.tr('_', '-'), &block)
route_meth = :"#{name}_route"
@@ -152,6 +153,21 @@ module Rodauth
FEATURES[name] = feature
end
+ def internal_request_method(name=feature_name)
+ (@internal_request_methods ||= []) << name
+ end
+
+ def class_methods(&block)
+ if defined?(self::ClassMethods)
+ mod = self::ClassMethods
+ else
+ mod = Module.new
+ const_set(:ClassMethods, mod)
+ end
+ mod.module_eval(&block)
+ nil
+ end
+
def configuration_module_eval(&block)
configuration.module_eval(&block)
end
@@ -254,36 +270,54 @@ module Rodauth
end
end
- class Auth
- class << self
- attr_accessor :roda_class
- attr_reader :features
- attr_reader :routes
- attr_accessor :route_hash
+ module AuthClassMethods
+ attr_accessor :roda_class
+ attr_reader :features
+ attr_reader :routes
+ attr_reader :internal_request_methods
+ attr_accessor :route_hash
+ attr_reader :configuration_name
+ attr_reader :configuration
+
+ def initialize_clone(klass)
+ super
+ @roda_class = klass.roda_class
+ @features = klass.features.clone
+ @routes = klass.routes.clone
+ @internal_request_methods = klass.internal_request_methods.clone
+ @route_hash = klass.route_hash.clone
+ @configuration = klass.configuration.clone
+ @configuration.instance_variable_set(:@auth, self)
end
- def self.inherited(subclass)
+ def inherited(subclass)
super
subclass.instance_exec do
@features = []
@routes = []
+ @internal_request_methods = []
@route_hash = {}
@configuration = Configuration.new(self)
end
end
- def self.configure(&block)
+ def configure(&block)
@configuration.apply(&block)
end
- def self.freeze
+ def freeze
@features.freeze
@routes.freeze
+ @internal_request_methods.freeze
@route_hash.freeze
super
end
end
+ class Auth
+ extend AuthClassMethods
+ end
+
class Configuration
attr_reader :auth
@@ -320,6 +354,14 @@ module Rodauth
@auth.routes.concat(feature.routes)
@auth.send(:include, feature)
+
+ if defined?(feature::ClassMethods)
+ @auth.extend(feature::ClassMethods)
+ end
+
+ if feature.internal_request_methods
+ @auth.internal_request_methods.concat(feature.internal_request_methods)
+ end
end
end
diff --git a/lib/rodauth/features/base.rb b/lib/rodauth/features/base.rb
index 2283436..fb9b2eb 100644
--- a/lib/rodauth/features/base.rb
+++ b/lib/rodauth/features/base.rb
@@ -734,6 +734,10 @@ module Rodauth
end
end
+ def internal_request?
+ false
+ end
+
def set_session_value(key, value)
session[key] = value
end
diff --git a/lib/rodauth/features/change_login.rb b/lib/rodauth/features/change_login.rb
index 3245f1b..8011ed5 100644
--- a/lib/rodauth/features/change_login.rb
+++ b/lib/rodauth/features/change_login.rb
@@ -19,6 +19,8 @@ module Rodauth
auth_methods :change_login
+ internal_request_method
+
route do |r|
require_account
before_change_login_route
diff --git a/lib/rodauth/features/change_password.rb b/lib/rodauth/features/change_password.rb
index 4aeeac0..23eeb51 100644
--- a/lib/rodauth/features/change_password.rb
+++ b/lib/rodauth/features/change_password.rb
@@ -22,6 +22,8 @@ module Rodauth
:invalid_previous_password_message
)
+ internal_request_method
+
route do |r|
require_account
before_change_password_route
diff --git a/lib/rodauth/features/close_account.rb b/lib/rodauth/features/close_account.rb
index 817f9aa..d5df8ca 100644
--- a/lib/rodauth/features/close_account.rb
+++ b/lib/rodauth/features/close_account.rb
@@ -24,6 +24,8 @@ module Rodauth
:delete_account
)
+ internal_request_method
+
route do |r|
require_account
before_close_account_route
diff --git a/lib/rodauth/features/create_account.rb b/lib/rodauth/features/create_account.rb
index 0f3aafb..3645e2b 100644
--- a/lib/rodauth/features/create_account.rb
+++ b/lib/rodauth/features/create_account.rb
@@ -27,6 +27,8 @@ module Rodauth
:new_account
)
+ internal_request_method
+
route do |r|
check_already_logged_in
before_create_account_route
diff --git a/lib/rodauth/features/email_auth.rb b/lib/rodauth/features/email_auth.rb
index 63f28be..7aae4c2 100644
--- a/lib/rodauth/features/email_auth.rb
+++ b/lib/rodauth/features/email_auth.rb
@@ -49,6 +49,10 @@ module Rodauth
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
diff --git a/lib/rodauth/features/internal_request.rb b/lib/rodauth/features/internal_request.rb
new file mode 100644
index 0000000..21e6609
--- /dev/null
+++ b/lib/rodauth/features/internal_request.rb
@@ -0,0 +1,287 @@
+# frozen-string-literal: true
+
+require 'stringio'
+
+module Rodauth
+ class InternalRequestError < StandardError
+ end
+
+ module InternalRequestMethods
+ attr_accessor :session
+ attr_accessor :params
+ attr_reader :flash
+ attr_reader :internal_request_return_value
+
+ def raw_param(k)
+ @params[k]
+ end
+
+ def set_error_flash(message)
+ @flash = message
+ _handle_internal_request_error(message)
+ end
+ alias set_redirect_error_flash set_error_flash
+ alias set_error_reason set_error_flash
+ private :set_error_reason
+
+ def set_notice_flash(message)
+ @flash = message
+ end
+ alias set_notice_now_flash set_notice_flash
+
+ def modifications_require_password?
+ false
+ end
+ alias require_login_confirmation? modifications_require_password?
+ alias require_password_confirmation? modifications_require_password?
+
+ def otp_setup_view
+ _return_from_internal_request([otp_user_key, otp_key])
+ end
+
+ def add_recovery_codes_view
+ _return_from_internal_request(recovery_codes)
+ end
+
+ private
+
+ def internal_request?
+ true
+ end
+
+ def after_login
+ super
+ _set_internal_request_return_value(account_id) unless @return_false_on_error
+ end
+
+ def after_remember
+ super
+ if params[remember_param] == remember_remember_param_value
+ _set_internal_request_return_value("#{account_id}_#{convert_token_key(remember_key_value)}")
+ end
+ end
+
+ def after_load_memory
+ super
+ _set_internal_request_return_value(session_value)
+ end
+
+ def before_change_password_route
+ super
+ params[new_password_param] ||= params[password_param]
+ end
+
+ def before_email_auth_request_route
+ super
+ _set_login_param_from_account
+ end
+
+ def before_login_route
+ super
+ _set_login_param_from_account
+ end
+
+ def before_unlock_account_request_route
+ super
+ _set_login_param_from_account
+ end
+
+ def before_reset_password_request_route
+ super
+ _set_login_param_from_account
+ end
+
+ def before_verify_account_resend_route
+ super
+ _set_login_param_from_account
+ end
+
+ def account_from_key(token, status_id=nil)
+ return super unless session_value
+ return unless yield session_value
+ ds = account_ds(session_value)
+ ds = ds.where(account_status_column=>status_id) if status_id && !skip_status_checks?
+ ds.first
+ end
+
+ def _set_internal_request_return_value(value)
+ @internal_request_return_value = value
+ end
+
+ def _return_from_internal_request(value)
+ _set_internal_request_return_value(value)
+ throw(:halt)
+ end
+
+ def _handle_internal_request_error(message)
+ if @return_false_on_error
+ _return_from_internal_request(false)
+ else
+ raise InternalRequestError, message
+ end
+ end
+
+ def _return_false_on_error!
+ @return_false_on_error = true
+ end
+
+ def _set_login_param_from_account
+ if session_value && !params[login_param] && (account = account_ds(session_value).first)
+ params[login_param] = account[login_column]
+ end
+ end
+
+ def _get_remember_cookie
+ params[remember_param]
+ end
+
+ def _handle_lock_account(_)
+ @account = {account_id_column=>session_value}
+ raised_uniqueness_violation{account_lockouts_ds.insert(_setup_account_lockouts_hash(account_id, generate_unlock_account_key))}
+ end
+
+ def _handle_remember_setup(request)
+ params[remember_param] = remember_remember_param_value
+ _handle_remember(request)
+ end
+
+ def _handle_remember_disable(request)
+ params[remember_param] = remember_disable_param_value
+ _handle_remember(request)
+ end
+
+ def _handle_account_id_for_remember_key(request)
+ load_memory
+ end
+
+ def _handle_otp_setup_params(request)
+ request.env['REQUEST_METHOD'] = 'GET'
+ _handle_otp_setup(request)
+ end
+
+ def _predicate_internal_request(meth, request)
+ _return_false_on_error!
+ _set_internal_request_return_value(true)
+ send(meth, request)
+ end
+
+ def _handle_valid_login_and_password?(request)
+ _predicate_internal_request(:_handle_login, request)
+ end
+
+ def _handle_valid_email_auth?(request)
+ _predicate_internal_request(:_handle_email_auth, request)
+ end
+
+ def _handle_valid_otp_auth?(request)
+ _predicate_internal_request(:_handle_otp_auth, request)
+ end
+
+ def _handle_valid_recovery_auth?(request)
+ _predicate_internal_request(:_handle_recovery_auth, request)
+ end
+
+ def _handle_valid_sms_auth?(request)
+ _predicate_internal_request(:_handle_sms_auth, request)
+ end
+ end
+
+ Feature.define(:internal_request, :InternalRequest) do
+ class_methods do
+ attr_reader :internal_request_configuration_blocks
+
+ def internal_request(route, opts={})
+ opts = opts.dup
+
+ env = {
+ 'REQUEST_METHOD'=>'POST',
+ 'PATH_INFO'=>'/',
+ "SCRIPT_NAME" => "",
+ "HTTP_HOST" => "invalidurl @@.com",
+ "SERVER_NAME" => 'invalidurl @@.com',
+ "CONTENT_TYPE" => "application/x-www-form-urlencoded",
+ "rack.input"=>StringIO.new(''),
+ "rack.url_scheme"=>"https"
+ }
+ env.merge!(opts.delete(:env)) if opts[:env]
+
+ session = {}
+ session.merge!(opts.delete(:session)) if opts[:session]
+
+ params = {}
+
+ rodauth = roda_class.new(env).rodauth(configuration_name)
+ rodauth.session = session
+ rodauth.params = params
+
+ unless account_id = opts.delete(:account_id)
+ if (account_login = opts.delete(:account_login))
+ if (account = rodauth.send(:_account_from_login, account_login))
+ account_id = account[rodauth.account_id_column]
+ else
+ raise InternalRequestError, "no account for login: #{account_login.inspect}"
+ end
+ end
+ end
+
+ if account_id
+ session[rodauth.session_key] = account_id
+ unless authenticated_by = opts.delete(:authenticated_by)
+ authenticated_by = case route
+ when :otp_auth, :sms_request, :sms_auth, :recovery_auth, :valid_otp_auth?, :valid_sms_auth?, :valid_recovery_auth?
+ ['internal1']
+ else
+ ['internal1', 'internal2']
+ end
+ end
+ session[rodauth.authenticated_by_session_key] = authenticated_by
+ end
+
+ opts.keys.each do |k|
+ meth = :"#{k}_param"
+ params[rodauth.public_send(meth).to_s] = opts.delete(k) if rodauth.respond_to?(meth)
+ end
+
+ unless opts.empty?
+ warn "unhandled options passed to #{route}: #{opts.inspect}"
+ end
+
+ catch(:halt) do
+ rodauth.send(:"_handle_#{route}", rodauth.request)
+ end
+
+ rodauth.internal_request_return_value
+ end
+ end
+
+ configuration_module_eval do
+ def internal_request_configuration(&block)
+ @auth.instance_exec do
+ (@internal_request_configuration_blocks ||= []) << block
+ end
+ end
+ end
+
+ def post_configure
+ super
+
+ klass = self.class
+ internal_class = klass.clone
+ internal_class.send(:include, InternalRequestMethods)
+ configuration_name = Object.new
+ internal_class.instance_variable_set(:@configuration_name, configuration_name)
+ klass.roda_class.plugin(:rodauth, :name=>configuration_name, :auth_class=>internal_class)
+
+ if blocks = klass.internal_request_configuration_blocks
+ configuration = internal_class.configuration
+ blocks.each do |block|
+ configuration.instance_exec(&block)
+ end
+ end
+
+ klass.internal_request_methods.each do |name|
+ klass.define_singleton_method(name){|opts={}| internal_class.internal_request(name, opts)}
+ end
+ end
+ end
+end
diff --git a/lib/rodauth/features/lockout.rb b/lib/rodauth/features/lockout.rb
index 04ab989..189ec54 100644
--- a/lib/rodauth/features/lockout.rb
+++ b/lib/rodauth/features/lockout.rb
@@ -62,6 +62,10 @@ module Rodauth
)
auth_private_methods :account_from_unlock_key
+ internal_request_method(:lock_account)
+ internal_request_method(:unlock_account_request)
+ internal_request_method(:unlock_account)
+
route(:unlock_account_request) do |r|
check_already_logged_in
before_unlock_account_request_route
@@ -167,6 +171,12 @@ module Rodauth
unlock_account
end
+ def _setup_account_lockouts_hash(account_id, key)
+ hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>key}
+ set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
+ hash
+ end
+
def invalid_login_attempted
ds = account_login_failures_ds.
where(account_login_failures_id_column=>account_id)
@@ -192,8 +202,7 @@ module Rodauth
if number >= max_invalid_logins
@unlock_account_key_value = generate_unlock_account_key
- hash = {account_lockouts_id_column=>account_id, account_lockouts_key_column=>unlock_account_key_value}
- set_deadline_value(hash, account_lockouts_deadline_column, account_lockouts_deadline_interval)
+ hash = _setup_account_lockouts_hash(account_id, unlock_account_key_value)
if e = raised_uniqueness_violation{account_lockouts_ds.insert(hash)}
# If inserting into the lockout table raises a violation, we should just be able to pull the already inserted
diff --git a/lib/rodauth/features/login.rb b/lib/rodauth/features/login.rb
index e3935da..6edc771 100644
--- a/lib/rodauth/features/login.rb
+++ b/lib/rodauth/features/login.rb
@@ -25,6 +25,9 @@ module Rodauth
auth_value_methods :login_return_to_requested_location_path
+ internal_request_method
+ internal_request_method :valid_login_and_password?
+
route do |r|
check_already_logged_in
before_login_route
diff --git a/lib/rodauth/features/otp.rb b/lib/rodauth/features/otp.rb
index 269b23b..eeb1497 100644
--- a/lib/rodauth/features/otp.rb
+++ b/lib/rodauth/features/otp.rb
@@ -96,6 +96,12 @@ module Rodauth
:otp_tmp_key
)
+ internal_request_method :otp_setup_params
+ internal_request_method :otp_setup
+ internal_request_method :otp_auth
+ internal_request_method :valid_otp_auth?
+ internal_request_method :otp_disable
+
route(:otp_auth) do |r|
require_login
require_account_session
diff --git a/lib/rodauth/features/recovery_codes.rb b/lib/rodauth/features/recovery_codes.rb
index 2a396e3..4840f66 100644
--- a/lib/rodauth/features/recovery_codes.rb
+++ b/lib/rodauth/features/recovery_codes.rb
@@ -59,6 +59,10 @@ module Rodauth
:recovery_code_match?,
)
+ internal_request_method :recovery_codes
+ internal_request_method :recovery_auth
+ internal_request_method :valid_recovery_auth?
+
route(:recovery_auth) do |r|
require_login
require_account_session
diff --git a/lib/rodauth/features/remember.rb b/lib/rodauth/features/remember.rb
index bce0725..d95983d 100644
--- a/lib/rodauth/features/remember.rb
+++ b/lib/rodauth/features/remember.rb
@@ -46,6 +46,10 @@ module Rodauth
:remove_remember_key
)
+ internal_request_method :remember_setup
+ internal_request_method :remember_disable
+ internal_request_method :account_id_for_remember_key
+
route do |r|
require_account
before_remember_route
@@ -83,7 +87,7 @@ module Rodauth
end
def remembered_session_id
- return unless cookie = request.cookies[remember_cookie_key]
+ return unless cookie = _get_remember_cookie
id, key = cookie.split('_', 2)
return unless id && key
@@ -110,7 +114,7 @@ module Rodauth
unless id = remembered_session_id
# Only set expired cookie if there is already a cookie set.
- forget_login if request.cookies[remember_cookie_key]
+ forget_login if _get_remember_cookie
return
end
@@ -187,6 +191,10 @@ module Rodauth
private
+ def _get_remember_cookie
+ request.cookies[remember_cookie_key]
+ end
+
def after_logout
forget_login
super if defined?(super)
diff --git a/lib/rodauth/features/reset_password.rb b/lib/rodauth/features/reset_password.rb
index 495cc87..697321e 100644
--- a/lib/rodauth/features/reset_password.rb
+++ b/lib/rodauth/features/reset_password.rb
@@ -57,6 +57,9 @@ module Rodauth
:account_from_reset_password_key
)
+ internal_request_method(:reset_password_request)
+ internal_request_method
+
route(:reset_password_request) do |r|
check_already_logged_in
before_reset_password_request_route
diff --git a/lib/rodauth/features/sms_codes.rb b/lib/rodauth/features/sms_codes.rb
index f757b3e..afeb29c 100644
--- a/lib/rodauth/features/sms_codes.rb
+++ b/lib/rodauth/features/sms_codes.rb
@@ -112,6 +112,13 @@ module Rodauth
:sms_valid_phone?
)
+ internal_request_method :sms_setup
+ internal_request_method :sms_confirm
+ internal_request_method :sms_request
+ internal_request_method :sms_auth
+ internal_request_method :valid_sms_auth?
+ internal_request_method :sms_disable
+
route(:sms_request) do |r|
require_login
require_account_session
diff --git a/lib/rodauth/features/two_factor_base.rb b/lib/rodauth/features/two_factor_base.rb
index afedd73..d92640c 100644
--- a/lib/rodauth/features/two_factor_base.rb
+++ b/lib/rodauth/features/two_factor_base.rb
@@ -57,6 +57,8 @@ module Rodauth
:two_factor_update_session
)
+ internal_request_method :two_factor_disable
+
route(:two_factor_manage, 'multifactor-manage') do |r|
require_account
before_two_factor_manage_route
diff --git a/lib/rodauth/features/verify_account.rb b/lib/rodauth/features/verify_account.rb
index 4f3a439..81795e1 100644
--- a/lib/rodauth/features/verify_account.rb
+++ b/lib/rodauth/features/verify_account.rb
@@ -60,6 +60,9 @@ module Rodauth
:account_from_verify_account_key
)
+ internal_request_method(:verify_account_resend)
+ internal_request_method
+
route(:verify_account_resend) do |r|
verify_account_check_already_logged_in
before_verify_account_resend_route
diff --git a/lib/rodauth/features/verify_login_change.rb b/lib/rodauth/features/verify_login_change.rb
index c022fe8..e903cbc 100644
--- a/lib/rodauth/features/verify_login_change.rb
+++ b/lib/rodauth/features/verify_login_change.rb
@@ -50,6 +50,8 @@ module Rodauth
:account_from_verify_login_change_key
)
+ internal_request_method
+
route do |r|
before_verify_login_change_route
diff --git a/spec/change_login_spec.rb b/spec/change_login_spec.rb
index a300e23..26a1e26 100644
--- a/spec/change_login_spec.rb
+++ b/spec/change_login_spec.rb
@@ -169,4 +169,31 @@ describe 'Rodauth change_login feature' do
json_login(:login=>'foo4@example.com')
end
end
+
+ it "should support changing logins using an internal request" do
+ rodauth do
+ enable :login, :change_login, :internal_request
+ login_meets_requirements?{|login| login.length > 4}
+ end
+ roda do |r|
+ r.rodauth
+ r.root{rodauth.logged_in?.nil?.to_s}
+ end
+
+ proc do
+ app.rodauth.change_login(:account_login=>'foo@example.com', :login=>'foo')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.change_login(:account_login=>'foo@example.com', :login=>'foo3@example.com').must_be_nil
+
+ visit '/'
+ page.body.must_equal 'true'
+
+ login
+ page.current_path.must_equal '/login'
+
+ login(:login=>'foo3@example.com', :visit=>false)
+ page.current_path.must_equal '/'
+ page.body.must_equal 'false'
+ end
end
diff --git a/spec/change_password_spec.rb b/spec/change_password_spec.rb
index ae67bc8..31a9782 100644
--- a/spec/change_password_spec.rb
+++ b/spec/change_password_spec.rb
@@ -201,4 +201,41 @@ describe 'Rodauth change_password feature' do
json_login(:pass=>'012345678')
end
end
+
+ it "should support changing passwords using an internal request" do
+ rodauth do
+ enable :login, :logout, :change_password, :internal_request
+ end
+ roda do |r|
+ r.rodauth
+ r.root{rodauth.logged_in?.nil?.to_s}
+ end
+
+ proc do
+ app.rodauth.change_password(:account_login=>'foo@example.com', :new_password=>'foo')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.change_password(:account_login=>'foo@example.com', :password=>'foo')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.change_password(:account_login=>'foo@example.com', :new_password=>'0123456').must_be_nil
+
+ visit '/'
+ page.body.must_equal 'true'
+
+ login
+ page.current_path.must_equal '/login'
+
+ login(:pass=>'0123456')
+ page.current_path.must_equal '/'
+ page.body.must_equal 'false'
+ logout
+
+ app.rodauth.change_password(:account_login=>'foo@example.com', :password=>'01234567').must_be_nil
+
+ login(:pass=>'01234567')
+ page.current_path.must_equal '/'
+ page.body.must_equal 'false'
+ end
end
diff --git a/spec/close_account_spec.rb b/spec/close_account_spec.rb
index c82bbdd..a16b6fb 100644
--- a/spec/close_account_spec.rb
+++ b/spec/close_account_spec.rb
@@ -161,4 +161,29 @@ describe 'Rodauth close_account feature' do
DB[:accounts].select_map(:status_id).must_equal [3]
end
end
+
+ it "should support closing accounts using an internal request" do
+ rodauth do
+ enable :login, :logout, :close_account, :internal_request
+ end
+ roda do |r|
+ r.rodauth
+ r.root{rodauth.logged_in?.nil?.to_s}
+ end
+
+ visit '/'
+ page.body.must_equal 'true'
+
+ login
+ page.body.must_equal 'false'
+
+ logout
+
+ app.rodauth.close_account(:account_login=>'foo@example.com').must_be_nil
+
+ login
+ page.current_path.must_equal '/login'
+
+ DB[:accounts].select_map(:status_id).must_equal [3]
+ end
end
diff --git a/spec/create_account_spec.rb b/spec/create_account_spec.rb
index 1e20ba4..9e23441 100644
--- a/spec/create_account_spec.rb
+++ b/spec/create_account_spec.rb
@@ -134,4 +134,28 @@ describe 'Rodauth create_account feature' do
json_login(:login=>'foo@example2.com')
end
end
+
+ it "should support creating accounts using an internal request" do
+ rodauth do
+ enable :login, :create_account, :internal_request
+ end
+ roda do |r|
+ r.rodauth
+ r.root{rodauth.logged_in?.nil?.to_s}
+ end
+
+ proc do
+ app.rodauth.create_account(:login=>'foo', :password=>'sdkjnlsalkklsda')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.create_account(:login=>'foo3@example.com', :password=>'123')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.create_account(:login=>'foo3@example.com', :password=>'sdkjnlsalkklsda').must_be_nil
+
+ login(:login=>'foo3@example.com', :pass=>'sdkjnlsalkklsda')
+ page.current_path.must_equal '/'
+ page.body.must_equal 'false'
+ end
end
diff --git a/spec/email_auth_spec.rb b/spec/email_auth_spec.rb
index 91380c2..823474d 100644
--- a/spec/email_auth_spec.rb
+++ b/spec/email_auth_spec.rb
@@ -340,4 +340,53 @@ describe 'Rodauth email auth feature' do
res.must_equal [200, {"success"=>"You have been logged in"}]
end
end
+
+ it "should allow checking email auth using internal requests" do
+ rodauth do
+ enable :login, :logout, :email_auth, :internal_request
+ end
+ roda do |r|
+ r.rodauth
+ r.root{view :content=>""}
+ end
+
+ visit '/login'
+ page.title.must_equal 'Login'
+ fill_in 'Login', :with=>'foo@example.com'
+ click_button 'Login'
+ click_button 'Send Login Link Via Email'
+ link = email_link(/(\/email-auth\?key=.+)$/)
+ key = link.split('=').last
+
+ app.rodauth.valid_email_auth?(:email_auth_key=>key[0...-1]).must_equal false
+ app.rodauth.valid_email_auth?(:email_auth_key=>key).must_equal true
+
+ visit link
+ page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
+
+ app.rodauth.email_auth_request(:account_login=>'foo@example.com').must_be_nil
+ link2 = email_link(/(\/email-auth\?key=.+)$/)
+ link2.wont_equal link
+ key = link2.split('=').last
+
+ proc do
+ app.rodauth.email_auth(:email_auth_key=>key[0...-1])
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.email_auth(:email_auth_key=>key).must_equal DB[:accounts].get(:id)
+
+ visit link
+ page.find('#error_flash').text.must_equal "There was an error logging you in: invalid email authentication key"
+
+ app.rodauth.email_auth_request(:account_login=>'foo@example.com').must_be_nil
+ link3 = email_link(/(\/email-auth\?key=.+)$/)
+ link3.wont_equal link
+ link3.wont_equal link2
+
+ visit link3
+ page.title.must_equal 'Login'
+ click_button 'Login'
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ page.current_path.must_equal '/'
+ end
end
diff --git a/spec/lockout_spec.rb b/spec/lockout_spec.rb
index c4fc31d..3fa970c 100644
--- a/spec/lockout_spec.rb
+++ b/spec/lockout_spec.rb
@@ -278,4 +278,87 @@ describe 'Rodauth lockout feature' do
json_login
end
end
+
+ it "should support account locks, unlocks, and unlock requests using internal requests" do
+ rodauth do
+ enable :lockout, :logout, :internal_request
+ account_lockouts_email_last_sent_column nil
+ end
+ roda do |r|
+ r.rodauth
+ r.root{view :content=>(rodauth.logged_in? ? "Logged In" : "Not Logged")}
+ end
+
+ proc do
+ app.rodauth.lock_account(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.unlock_account_request(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.unlock_account(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.unlock_account_request(:login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.unlock_account_request(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.unlock_account(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.lock_account(:account_login=>'foo@example.com').must_be_nil
+
+ # Check idempotent
+ app.rodauth.lock_account(:account_login=>'foo@example.com').must_be_nil
+
+ login
+ page.find('#error_flash').text.must_equal "This account is currently locked out and cannot be logged in to"
+
+ app.rodauth.unlock_account_request(:login=>'foo@example.com').must_be_nil
+ link = email_link(/(\/unlock-account\?key=.+)$/)
+
+ app.rodauth.unlock_account_request(:account_login=>'foo@example.com').must_be_nil
+ link2 = email_link(/(\/unlock-account\?key=.+)$/)
+ link2.must_equal link
+
+ visit link
+ click_button 'Unlock Account'
+
+ page.find('#notice_flash').text.must_equal 'Your account has been unlocked'
+ page.body.must_include("Logged In")
+
+ logout
+
+ app.rodauth.lock_account(:account_login=>'foo@example.com').must_be_nil
+
+ login
+ page.find('#error_flash').text.must_equal "This account is currently locked out and cannot be logged in to"
+
+ app.rodauth.unlock_account(:account_login=>'foo@example.com').must_be_nil
+
+ login
+ page.body.must_include 'Logged In'
+
+ app.rodauth.lock_account(:account_login=>'foo@example.com').must_be_nil
+ app.rodauth.unlock_account_request(:account_login=>'foo@example.com').must_be_nil
+ link3 = email_link(/(\/unlock-account\?key=.+)$/)
+ link3.wont_equal link2
+ key = link3.split('=').last
+
+ proc do
+ app.rodauth.unlock_account(:unlock_account_key=>key[0...-1])
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.unlock_account(:unlock_account_key=>key).must_be_nil
+
+ login
+ page.body.must_include 'Logged In'
+ end
end
diff --git a/spec/login_spec.rb b/spec/login_spec.rb
index 097f7eb..3dd79dc 100644
--- a/spec/login_spec.rb
+++ b/spec/login_spec.rb
@@ -438,4 +438,39 @@ describe 'Rodauth login feature' do
json_request.must_equal [200, 2]
end
end
+
+ it "should allow checking login and password using internal requests" do
+ rodauth do
+ enable :login, :internal_request
+ end
+ roda do |r|
+ end
+
+ app.rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'0123456789').must_equal true
+ app.rodauth.valid_login_and_password?(:login=>'foo@example.com', :password=>'012345678').must_equal false
+ app.rodauth.valid_login_and_password?(:login=>'foo@example2.com', :password=>'0123456789').must_equal false
+
+ app.rodauth.valid_login_and_password?(:account_login=>'foo@example.com', :password=>'0123456789').must_equal true
+ app.rodauth.valid_login_and_password?(:account_login=>'foo@example.com', :password=>'012345678').must_equal false
+
+ proc do
+ app.rodauth.valid_login_and_password?(:account_login=>'foo@example2.com', :password=>'0123456789')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.login(:account_login=>'foo@example.com', :password=>'0123456789').must_equal DB[:accounts].get(:id)
+
+ proc do
+ app.rodauth.login(:login=>'foo@example.com', :password=>'012345678')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.login(:login=>'foo@example2.com', :password=>'0123456789')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.login(:account_login=>'foo@example.com', :password=>'0123456789').must_equal DB[:accounts].get(:id)
+
+ proc do
+ app.rodauth.login(:account_login=>'foo@example.com', :password=>'012345678')
+ end.must_raise Rodauth::InternalRequestError
+ end
end
diff --git a/spec/remember_spec.rb b/spec/remember_spec.rb
index 2f23424..1a64d32 100644
--- a/spec/remember_spec.rb
+++ b/spec/remember_spec.rb
@@ -559,4 +559,62 @@ describe 'Rodauth remember feature' do
json_request.must_equal [200, [3]]
end
end
+
+ it "should support remember token management via internal requests" do
+ key = nil
+ rodauth do
+ enable :login, :logout, :remember, :internal_request
+ hmac_secret '123'
+ end
+ roda do |r|
+ r.rodauth
+ r.get 'setup' do
+ key = rodauth.class.remember_setup(:account_login=>'foo@example.com')
+ ::Rack::Utils.set_cookie_header!(response.headers, '_remember', :value=>key)
+ ''
+ end
+ r.get 'load' do
+ rodauth.load_memory
+ r.redirect '/'
+ end
+ r.get 'loadpage' do
+ rodauth.load_memory
+ ''
+ end
+ r.root do
+ if rodauth.logged_in?
+ if rodauth.logged_in_via_remember_key?
+ view :content=>"Logged In via Remember"
+ else
+ view :content=>"Logged In Normally"
+ end
+ else
+ view :content=>"Not Logged In"
+ end
+ end
+ end
+
+ visit '/setup'
+ page.body.must_equal ''
+
+ app.rodauth.account_id_for_remember_key(:remember=>key).must_equal DB[:accounts].get(:id)
+ app.rodauth.account_id_for_remember_key(:remember=>key[0...-1]).must_be_nil
+
+ visit '/'
+ page.body.must_include 'Not Logged In'
+
+ visit '/load'
+ page.body.must_include 'Logged In via Remember'
+
+ logout
+
+ visit '/setup'
+ page.body.must_equal ''
+
+ app.rodauth.remember_disable(:account_login=>'foo@example.com').must_be_nil
+ app.rodauth.account_id_for_remember_key(:remember=>key).must_be_nil
+
+ visit '/load'
+ page.body.must_include 'Not Logged In'
+ end
end
diff --git a/spec/reset_password_spec.rb b/spec/reset_password_spec.rb
index 8ba9daa..375f571 100644
--- a/spec/reset_password_spec.rb
+++ b/spec/reset_password_spec.rb
@@ -289,4 +289,82 @@ describe 'Rodauth reset_password feature' do
json_login(:pass=>'0123456')
end
end
+
+ it "should support requesting password resets using an internal request" do
+ rodauth do
+ enable :login, :logout, :reset_password, :internal_request
+ reset_password_email_last_sent_column nil
+ end
+ roda do |r|
+ r.rodauth
+ r.root{view :content=>(rodauth.logged_in? ? "Logged In" : "Not Logged")}
+ end
+
+ proc do
+ app.rodauth.reset_password_request(:login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.reset_password_request(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.reset_password(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.reset_password(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.reset_password_request(:login=>'foo@example.com').must_be_nil
+ link = email_link(/(\/reset-password\?key=.+)$/)
+
+ app.rodauth.reset_password_request(:account_login=>'foo@example.com').must_be_nil
+ link2 = email_link(/(\/reset-password\?key=.+)$/)
+ link2.must_equal link
+
+ visit link
+ fill_in 'Password', :with=>'0123456'
+ fill_in 'Confirm Password', :with=>'0123456'
+ click_button 'Reset Password'
+ page.find('#notice_flash').text.must_equal "Your password has been reset"
+
+ login(:pass=>'0123456')
+ page.body.must_include "Logged In"
+
+ logout
+
+ app.rodauth.reset_password_request(:account_login=>'foo@example.com').must_be_nil
+ email_link(/(\/reset-password\?key=.+)$/)
+ app.rodauth.reset_password(:account_login=>'foo@example.com', :password=>'01234567').must_be_nil
+
+ login(:pass=>'01234567')
+ page.body.must_include "Logged In"
+
+ logout
+
+ app.rodauth.reset_password_request(:login=>'foo@example.com').must_be_nil
+ link = email_link(/(\/reset-password\?key=.+)$/)
+
+ app.rodauth.reset_password(:account_login=>'foo@example.com', :password=>'012345678').must_be_nil
+
+ visit link
+ page.find('#error_flash').text.must_equal "There was an error resetting your password: invalid or expired password reset key"
+
+ login(:pass=>'012345678')
+ page.body.must_include "Logged In"
+
+ app.rodauth.reset_password_request(:login=>'foo@example.com').must_be_nil
+ link = email_link(/(\/reset-password\?key=.+)$/)
+ key = link.split('=').last
+
+ proc do
+ app.rodauth.reset_password(:reset_password_key=>key[0...-1], :password=>'0123456789').must_be_nil
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.reset_password(:reset_password_key=>key, :password=>'0123456789').must_be_nil
+
+ login(:pass=>'0123456789')
+ page.body.must_include "Logged In"
+ end
end
diff --git a/spec/rodauth_spec.rb b/spec/rodauth_spec.rb
index 94df9e5..2c65813 100644
--- a/spec/rodauth_spec.rb
+++ b/spec/rodauth_spec.rb
@@ -413,6 +413,26 @@ describe 'Rodauth' do
Rodauth::FEATURES.delete(:foo)
end
+ it "should support features with multiple class_methods blocks" do
+ require "rodauth"
+ Rodauth::Feature.define(:foo) do
+ class_methods{def a; 1; end}
+ class_methods{def b; 2 + a; end}
+ end
+
+ rodauth do
+ enable :foo
+ end
+ roda do |r|
+ self.class.rodauth.b.to_s
+ end
+
+ visit '/'
+ page.body.must_equal '3'
+
+ Rodauth::FEATURES.delete(:foo)
+ end
+
it "should support auth_class_eval for evaluation inside Auth class" do
rodauth do
enable :login
@@ -591,4 +611,117 @@ describe 'Rodauth' do
visit '/login'
hooks.must_equal [:before_around, :before, :after_around]
end
+
+ {
+ 'should allow different configuerations for internal requests'=>true,
+ 'should allow use of internal_request? to determine whether this is an internal request'=>false
+ }.each do |desc, use_internal_request_predicate|
+ it desc do
+ rodauth do
+ enable :login, :logout, :create_account, :internal_request
+ require_login_confirmation? false
+ require_password_confirmation? false
+
+ if use_internal_request_predicate
+ login_minimum_length{internal_request? ? 9 : 15}
+ password_minimum_length{internal_request? ? 3 : super()}
+ else
+ login_minimum_length 15
+
+ internal_request_configuration do
+ login_minimum_length 9
+ end
+
+ internal_request_configuration do
+ password_minimum_length 3
+ end
+ end
+ end
+ roda do |r|
+ r.rodauth
+ view :content=>""
+ end
+
+ visit '/create-account'
+ fill_in 'Login', :with=>'foo@e.com'
+ fill_in 'Password', :with=>'012'
+ click_button 'Create Account'
+ page.html.must_include("invalid login, minimum 15 characters")
+ page.find('#error_flash').text.must_equal "There was an error creating your account"
+
+ fill_in 'Login', :with=>'foo@e123456789.com'
+ fill_in 'Password', :with=>'012'
+ click_button 'Create Account'
+ page.html.must_include("invalid password, does not meet requirements (minimum 6 characters)")
+ page.find('#error_flash').text.must_equal "There was an error creating your account"
+
+ fill_in 'Password', :with=>'123456'
+ click_button 'Create Account'
+ page.find('#notice_flash').text.must_equal "Your account has been created"
+
+ login(:login=>'foo@e123456789.com', :pass=>'123456')
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ logout
+
+ app.rodauth.create_account(:login=>'foo@f.com', :password=>'012').must_be_nil
+
+ proc do
+ app.rodauth.create_account(:login=>'foo@e.com', :password=>'12')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.create_account(:login=>'f@e.com', :password=>'012')
+ end.must_raise Rodauth::InternalRequestError
+
+ login(:login=>'foo@f.com', :pass=>'012')
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ end
+ end
+
+ it "should allow custom options when creating internal requests" do
+ rodauth do
+ enable :login, :logout, :create_account, :change_login, :internal_request
+ before_create_account_route do
+ params[login_param] += request.env[:at] + session[:domain]
+ end
+ before_change_login_route do
+ params[login_param] += authenticated_by.first
+ end
+ end
+ roda do |r|
+ r.rodauth
+ view :content=>""
+ end
+
+ app.rodauth.create_account(:login=>'foo', :password=>'0123456789', :env=>{:at=>'@'}, :session=>{:domain=>'g.com'}).must_be_nil
+
+ login(:login=>'foo@g.com')
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ logout
+
+ app.rodauth.change_login(:account_id=>DB[:accounts].where(:email=>'foo@g.com').get(:id), :login=>'foo@h.', :authenticated_by=>['com']).must_be_nil
+
+ login(:login=>'foo@h.com')
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ end
+
+ it "should warn for invalid options for internal requests" do
+ warning = nil
+ rodauth do
+ enable :login, :logout, :create_account, :internal_request
+ auth_class_eval do
+ define_singleton_method(:warn){|*a| warning = a}
+ end
+ end
+ roda do |r|
+ r.rodauth
+ view :content=>""
+ end
+
+ app.rodauth.create_account(:login=>'foo@h.com', :password=>'0123456789', :banana=>:pear).must_be_nil
+ warning.must_equal ["unhandled options passed to create_account: {:banana=>:pear}"]
+
+ login(:login=>'foo@h.com')
+ page.find('#notice_flash').text.must_equal 'You have been logged in'
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 09404d0..abc0678 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -119,7 +119,7 @@ DB = Sequel.connect(db_url, :identifier_mangling=>false)
DB.extension :freeze_datasets, :date_arithmetic
puts "using #{DB.database_type}"
-#DB.loggers << Logger.new($stdout)
+DB.loggers << Logger.new($stdout) if ENV['LOG_SQL']
if DB.adapter_scheme == :jdbc
case DB.database_type
when :postgres
diff --git a/spec/two_factor_spec.rb b/spec/two_factor_spec.rb
index eaa2cac..19dbc8b 100644
--- a/spec/two_factor_spec.rb
+++ b/spec/two_factor_spec.rb
@@ -1641,6 +1641,250 @@ describe 'Rodauth OTP feature' do
page.html.must_include 'With OTP'
end
+ it "should allow using otp via internal requests" do
+ rodauth do
+ enable :login, :logout, :otp, :internal_request
+ hmac_secret '123'
+ end
+ roda do |r|
+ r.rodauth
+ r.redirect '/login' unless rodauth.logged_in?
+ r.redirect '/otp-setup' unless rodauth.two_factor_authentication_setup?
+ r.redirect '/otp-auth' unless rodauth.two_factor_authenticated?
+ view :content=>""
+ end
+
+ secret, raw_secret = app.rodauth.otp_setup_params(:account_login=>'foo@example.com')
+ totp = ROTP::TOTP.new(secret)
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret[0...-1], :otp_setup_raw=>raw_secret, :otp_auth=>totp.now)
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_setup_raw=>raw_secret[0...-1], :otp_auth=>totp.now)
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_setup_raw=>raw_secret, :otp_auth=>totp.now[0...-1])
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_setup_raw=>raw_secret, :otp_auth=>totp.now).must_be_nil
+ reset_otp_last_use
+
+ proc do
+ app.rodauth.otp_setup_params(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_setup_raw=>raw_secret, :otp_auth=>totp.now)
+ end.must_raise Rodauth::InternalRequestError
+
+ login
+ fill_in 'Authentication Code', :with=>totp.now
+ click_button 'Authenticate Using TOTP'
+ page.find('#notice_flash').text.must_equal 'You have been multifactor authenticated'
+ reset_otp_last_use
+
+ proc do
+ app.rodauth.otp_auth(:account_login=>'foo@example.com', :otp_auth=>totp.now[0...-1])
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.otp_auth(:account_login=>'foo@example.com', :otp_auth=>totp.now).must_be_nil
+ reset_otp_last_use
+
+ app.rodauth.valid_otp_auth?(:account_login=>'foo@example.com', :otp_auth=>totp.now[0...-1]).must_equal false
+ reset_otp_last_use
+
+ app.rodauth.valid_otp_auth?(:account_login=>'foo@example.com', :otp_auth=>totp.now).must_equal true
+ reset_otp_last_use
+
+ app.rodauth.otp_disable(:account_login=>'foo@example.com').must_be_nil
+
+ app.rodauth.valid_otp_auth?(:account_login=>'foo@example.com', :otp_auth=>totp.now[0...-1]).must_equal false
+
+ proc do
+ app.rodauth.otp_disable(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+ end
+
+ it "should allow using otp via internal requests without hmac" do
+ rodauth do
+ enable :login, :logout, :otp, :internal_request
+ end
+ roda do |r|
+ end
+
+ secret, raw_secret = app.rodauth.otp_setup_params(:account_login=>'foo@example.com')
+ raw_secret.must_equal secret
+ totp = ROTP::TOTP.new(secret)
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret[0...-1], :otp_auth=>totp.now)
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_auth=>totp.now[0...-1])
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_auth=>totp.now).must_be_nil
+ reset_otp_last_use
+
+ app.rodauth.valid_otp_auth?(:account_login=>'foo@example.com', :otp_auth=>totp.now).must_equal true
+ reset_otp_last_use
+ end
+
+ it "should allow using recovery codes via internal requests" do
+ rodauth do
+ enable :login, :logout, :recovery_codes, :internal_request
+ recovery_codes_primary? false
+ end
+ roda do |r|
+ r.rodauth
+ r.redirect '/login' unless rodauth.logged_in?
+ rodauth.require_two_factor_authenticated
+ view :content=>""
+ end
+
+ app.rodauth.recovery_codes(:account_login=>'foo@example.com').must_equal []
+
+ recovery_codes = app.rodauth.recovery_codes(:account_login=>'foo@example.com', :add_recovery_codes=>'1')
+ recovery_codes.length.must_equal 16
+
+ proc do
+ app.rodauth.recovery_auth(:account_login=>'foo@example.com', :recovery_codes=>'foo')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.recovery_auth(:account_login=>'foo@example.com', :recovery_codes=>recovery_codes.shift).must_be_nil
+
+ app.rodauth.valid_recovery_auth?(:account_login=>'foo@example.com', :recovery_codes=>'foo').must_equal false
+ app.rodauth.valid_recovery_auth?(:account_login=>'foo@example.com', :recovery_codes=>recovery_codes.shift).must_equal true
+
+ login
+
+ fill_in 'Recovery Code', :with=>recovery_codes.shift
+ click_button 'Authenticate via Recovery Code'
+ page.find('#notice_flash').text.must_equal 'You have been multifactor authenticated'
+
+ recovery_codes2 = app.rodauth.recovery_codes(:account_login=>'foo@example.com')
+ recovery_codes2.sort.must_equal recovery_codes.sort
+
+ recovery_codes3 = app.rodauth.recovery_codes(:account_login=>'foo@example.com', :add_recovery_codes=>'1')
+ recovery_codes3.length.must_equal 16
+ (recovery_codes & recovery_codes3).length.must_equal 13
+ end
+
+ it "should allow using sms codes via internal requests" do
+ sms_message = nil
+ rodauth do
+ enable :login, :logout, :sms_codes, :internal_request
+ sms_send do |phone, msg|
+ sms_message = msg
+ end
+ domain 'example.com'
+ end
+ roda do |r|
+ r.rodauth
+ rodauth.require_two_factor_authenticated
+ view :content=>""
+ end
+
+ app.rodauth.sms_setup(:account_login=>'foo@example.com', :sms_phone=>'1112223333').must_be_nil
+ sms_message.must_match(/\ASMS confirmation code for example\.com is \d{12}\z/)
+ sms_code = sms_message[/\d{12}\z/]
+
+ login
+ fill_in 'SMS Code', :with=>sms_code
+ click_button 'Confirm SMS Backup Number'
+ page.find('#notice_flash').text.must_equal 'SMS authentication has been setup'
+ logout
+
+ proc do
+ app.rodauth.sms_setup(:account_login=>'foo@example.com', :sms_phone=>'1112224444')
+ end.must_raise Rodauth::InternalRequestError
+
+ login
+ page.title.must_equal 'Send SMS Code'
+ click_button 'Send SMS Code'
+ sms_message.must_match(/\ASMS authentication code for example\.com is \d{6}\z/)
+ sms_code = sms_message[/\d{6}\z/]
+
+ fill_in 'SMS Code', :with=>sms_code
+ click_button 'Authenticate via SMS Code'
+ page.find('#notice_flash').text.must_equal 'You have been multifactor authenticated'
+ logout
+
+ app.rodauth.sms_disable(:account_login=>'foo@example.com').must_be_nil
+
+ proc do
+ app.rodauth.sms_disable(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ login
+ fill_in 'Password', :with=>'0123456789'
+ fill_in 'Phone Number', :with=>'(123) 456-7890'
+ click_button 'Setup SMS Backup Number'
+ page.find('#notice_flash').text.must_equal 'SMS authentication needs confirmation'
+ sms_message.must_match(/\ASMS confirmation code for example\.com is \d{12}\z/)
+ sms_code = sms_message[/\d{12}\z/]
+ logout
+
+ app.rodauth.sms_confirm(:account_login=>'foo@example.com', :sms_code=>sms_code).must_be_nil
+
+ proc do
+ app.rodauth.sms_confirm(:account_login=>'foo@example.com', :sms_code=>sms_code)
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.sms_request(:account_login=>'foo@example.com').must_be_nil
+ sms_message.must_match(/\ASMS authentication code for example\.com is \d{6}\z/)
+ sms_code = sms_message[/\d{6}\z/]
+ app.rodauth.sms_auth(:account_login=>'foo@example.com', :sms_code=>sms_code).must_be_nil
+
+ login
+ page.title.must_equal 'Send SMS Code'
+ click_button 'Send SMS Code'
+ sms_message.must_match(/\ASMS authentication code for example\.com is \d{6}\z/)
+ sms_code = sms_message[/\d{6}\z/]
+ logout
+
+ app.rodauth.valid_sms_auth?(:account_login=>'foo@example.com', :sms_code=>sms_code).must_equal true
+ app.rodauth.valid_sms_auth?(:account_login=>'foo@example.com', :sms_code=>sms_code).must_equal false
+ end
+
+ it "should allow removing all multifactor authentication via internal requests" do
+ sms_message = nil
+ rodauth do
+ enable :otp, :sms_codes, :recovery_codes, :internal_request
+ sms_send do |phone, msg|
+ sms_message = msg
+ end
+ domain 'example.com'
+ end
+ roda do |r|
+ end
+
+ secret, raw_secret = app.rodauth.otp_setup_params(:account_login=>'foo@example.com')
+ totp = ROTP::TOTP.new(secret)
+ app.rodauth.otp_setup(:account_login=>'foo@example.com', :otp_setup=>secret, :otp_setup_raw=>raw_secret, :otp_auth=>totp.now).must_be_nil
+
+ app.rodauth.sms_setup(:account_login=>'foo@example.com', :sms_phone=>'1112223333').must_be_nil
+ sms_message.must_match(/\ASMS confirmation code for example\.com is \d{12}\z/)
+ sms_code = sms_message[/\d{12}\z/]
+ app.rodauth.sms_confirm(:account_login=>'foo@example.com', :sms_code=>sms_code).must_be_nil
+ app.rodauth.sms_request(:account_login=>'foo@example.com').must_be_nil
+ sms_message.must_match(/\ASMS authentication code for example\.com is \d{6}\z/)
+ sms_code = sms_message[/\d{6}\z/]
+ app.rodauth.sms_auth(:account_login=>'foo@example.com', :sms_code=>sms_code).must_be_nil
+
+ recovery_codes = app.rodauth.recovery_codes(:account_login=>'foo@example.com', :add_recovery_codes=>'1')
+ app.rodauth.recovery_auth(:account_login=>'foo@example.com', :recovery_codes=>recovery_codes.shift).must_be_nil
+
+ app.rodauth.two_factor_disable(:account_login=>'foo@example.com').must_be_nil
+ [:account_otp_keys, :account_recovery_codes, :account_sms_codes].each do |t|
+ DB[t].count.must_equal 0
+ end
+ end
+
begin
require 'webauthn/fake_client'
rescue LoadError
diff --git a/spec/verify_account_spec.rb b/spec/verify_account_spec.rb
index cedae45..fa0b84f 100644
--- a/spec/verify_account_spec.rb
+++ b/spec/verify_account_spec.rb
@@ -312,4 +312,96 @@ describe 'Rodauth verify_account feature' do
json_login(:login=>'foo@example2.com')
end
end
+
+ it "should allow verifying accounts using internal requests" do
+ rodauth do
+ enable :login, :logout, :verify_account, :internal_request, :change_password
+ verify_account_email_last_sent_column nil
+ end
+ roda do |r|
+ r.rodauth
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
+ end
+
+ proc do
+ app.rodauth.verify_account_resend(:login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.verify_account(:account_login=>'foo3@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.verify_account_resend(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ proc do
+ app.rodauth.verify_account(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ visit '/create-account'
+ fill_in 'Login', :with=>'foo@example2.com'
+ click_button 'Create Account'
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
+ page.current_path.must_equal '/'
+ link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com')
+
+ app.rodauth.verify_account_resend(:account_login=>'foo@example2.com').must_be_nil
+ link2 = email_link(/(\/verify-account\?key=.+)$/, 'foo@example2.com')
+ link2.must_equal link
+
+ visit link
+ fill_in 'Password', :with=>'0123456789'
+ fill_in 'Confirm Password', :with=>'0123456789'
+ click_button 'Verify Account'
+ page.find('#notice_flash').text.must_equal "Your account has been verified"
+ page.body.must_include 'Logged In'
+ logout
+
+ login(:login=>'foo@example2.com')
+ page.body.must_include 'Logged In'
+ logout
+
+ visit '/create-account'
+ fill_in 'Login', :with=>'foo@example3.com'
+ click_button 'Create Account'
+ page.find('#notice_flash').text.must_equal "An email has been sent to you with a link to verify your account"
+ page.current_path.must_equal '/'
+ link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example3.com')
+
+ app.rodauth.verify_account_resend(:login=>'foo@example3.com').must_be_nil
+ link2 = email_link(/(\/verify-account\?key=.+)$/, 'foo@example3.com')
+ link2.must_equal link
+
+ app.rodauth.verify_account(:account_login=>'foo@example3.com', :password=>'0123456789').must_be_nil
+
+ login(:login=>'foo@example3.com')
+ page.body.must_include 'Logged In'
+ logout
+
+ app.rodauth.create_account(:login=>'foo@example4.com').must_be_nil
+ link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example4.com')
+
+ app.rodauth.verify_account_resend(:login=>'foo@example4.com').must_be_nil
+ link2 = email_link(/(\/verify-account\?key=.+)$/, 'foo@example4.com')
+ link2.must_equal link
+
+ app.rodauth.verify_account(:account_login=>'foo@example4.com', :password=>'0123456789').must_be_nil
+
+ login(:login=>'foo@example4.com')
+ page.body.must_include 'Logged In'
+
+ app.rodauth.create_account(:login=>'foo@example5.com').must_be_nil
+ link = email_link(/(\/verify-account\?key=.+)$/, 'foo@example5.com')
+ key = link.split('=').last
+
+ proc do
+ app.rodauth.verify_account(:verify_account_key=>key[0...-1], :password=>'0123456789')
+ end.must_raise Rodauth::InternalRequestError
+
+ app.rodauth.verify_account(:verify_account_key=>key, :password=>'0123456789').must_be_nil
+
+ login(:login=>'foo@example5.com')
+ page.body.must_include 'Logged In'
+ end
end
diff --git a/spec/verify_login_change_spec.rb b/spec/verify_login_change_spec.rb
index a9e5050..0d78f5d 100644
--- a/spec/verify_login_change_spec.rb
+++ b/spec/verify_login_change_spec.rb
@@ -232,4 +232,53 @@ describe 'Rodauth verify_login_change feature' do
json_login(:login=>'foo3@example.com')
end
end
+
+ it "should support verifying login changes using internal requests" do
+ rodauth do
+ enable :login, :logout, :verify_login_change, :internal_request
+ end
+ roda do |r|
+ r.rodauth
+ r.root{view :content=>rodauth.logged_in? ? "Logged In" : "Not Logged"}
+ end
+
+ proc do
+ app.rodauth.verify_login_change(:account_login=>'foo@example.com')
+ end.must_raise Rodauth::InternalRequestError
+
+ login
+
+ visit '/change-login'
+ fill_in 'Login', :with=>'foo@example2.com'
+ fill_in 'Password', :with=>'0123456789'
+ click_button 'Change Login'
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example2.com')
+
+ app.rodauth.verify_login_change(:account_login=>'foo@example.com').must_be_nil
+
+ visit link
+ page.find('#error_flash').text.must_equal "There was an error verifying your login change: invalid verify login change key"
+
+ login(:login=>'foo@example2.com')
+ page.body.must_include('Logged In')
+ logout
+
+ app.rodauth.change_login(:account_login=>'foo@example2.com', :login=>'foo@example3.com').must_be_nil
+ link = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example3.com')
+ app.rodauth.verify_login_change(:account_login=>'foo@example2.com').must_be_nil
+
+ visit link
+ page.find('#error_flash').text.must_equal "There was an error verifying your login change: invalid verify login change key"
+
+ login(:login=>'foo@example3.com')
+ page.body.must_include('Logged In')
+ logout
+
+ app.rodauth.change_login(:account_login=>'foo@example3.com', :login=>'foo@example4.com').must_be_nil
+ key = email_link(/(\/verify-login-change\?key=.+)$/, 'foo@example4.com').split('=').last
+ app.rodauth.verify_login_change(:verify_login_change_key=>key).must_be_nil
+
+ login(:login=>'foo@example4.com')
+ page.body.must_include('Logged In')
+ end
end
diff --git a/www/pages/documentation.erb b/www/pages/documentation.erb
index 7c5cbb4..ec51329 100644
--- a/www/pages/documentation.erb
+++ b/www/pages/documentation.erb
@@ -27,6 +27,7 @@
<li><a href="rdoc/files/doc/disallow_password_reuse_rdoc.html">Disallow Password Reuse</a>: Disallows setting password to the same string as previous passwords.</li>
<li><a href="rdoc/files/doc/email_auth_rdoc.html">Email Authentication</a>: Allows login via a link sent via email.</li>
<li><a href="rdoc/files/doc/http_basic_auth_rdoc.html">HTTP Basic Auth</a>: Allows HTTP basic authentication.</li>
+ <li><a href="rdoc/files/doc/internal_request_rdoc.html">Internal Request</a>: Allows interacting with Rodauth by calling methods.</li>
<li><a href="rdoc/files/doc/json_rdoc.html">JSON</a>: Adds JSON API support for all other features.</li>
<li><a href="rdoc/files/doc/jwt_rdoc.html">JWT</a>: Adds JSON Web Token support for all other features.</li>
<li><a href="rdoc/files/doc/jwt_cors_rdoc.html">JWT CORS</a>: Supports Cross-Origin Resource Sharing in the JSON API.</li>
diff --git a/www/pages/why.erb b/www/pages/why.erb
index 07d82d8..ed19627 100644
--- a/www/pages/why.erb
+++ b/www/pages/why.erb
@@ -80,6 +80,7 @@
<li>JWT (JSON Web Token support for all other features)</li>
<li>JWT CORS (Cross-Origin Resource Sharing)</li>
<li>JWT Refresh (Access &amp; refresh tokens)</li>
+ <li>Internal Request (Interact with Rodauth via methods)</li>
</ul>
<p style="margin-top: 20px;">You can learn more about these features by reviewing <a href="documentation.html">Rodauth's documentation</a>.</p>
--
2.31.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment