= Rodauth
Rodauth is an authentication and account management framework for
rack applications. It's built using Roda and Sequel, but it can
be used with other web frameworks, database libraries, and databases.
When used with PostgreSQL, MySQL, and Microsoft SQL Server in the
default configuration, it offers additional security for password
hashes by protecting access via database functions.
== Design Goals
* Security: Ship in a maximum security by default configuration
* Simplicity: Allow for easy configuration via a DSL
* Flexibility: Allow for easy overriding of any part of the framework
== Features
* Login
* Logout
* Change Password
* Change Login
* Reset Password
* Create Account
* Close Account
* Verify Account
* Confirm Password
* Remember (Autologin via token)
* Lockout (Bruteforce protection)
* OTP (2 factor authentication via TOTP)
* Recovery Codes (2 factor authentication via backup codes)
* SMS Codes (2 factor authentication via SMS)
* Verify Change Login (Reverify accounts after login changes)
* Verify Account Grace Period (Don't require verification before login)
* Password Grace Period (Don't require password entry if recently entered)
* Password Complexity (More sophisticated checks)
* Disallow Password Reuse
* Password Expiration
* Account Expiration
* Session Expiration
* Single Session (Only one active session per account)
* JWT (JSON API support for all other features)
== Resources
Website :: http://rodauth.jeremyevans.net
Demo Site :: http://rodauth-demo.jeremyevans.net
Source :: http://github.com/jeremyevans/rodauth
Bugs :: http://github.com/jeremyevans/rodauth/issues
Google Group :: https://groups.google.com/forum/#!forum/rodauth
IRC :: irc://chat.freenode.net/#rodauth
== Dependencies
There are some dependencies that Rodauth uses by default, but are
development dependencies instead of runtime dependencies in the
gem as it is possible to run without them:
tilt, rack_csrf :: Used by all features unless in JSON API only mode.
bcrypt :: Used by default for password matching, can be skipped
if password_match? is overridden for custom authentication.
mail :: Used by default for mailing in the reset password, verify
account, and lockout features.
rotp, rqrcode :: Used by the otp feature
jwt :: Used by the jwt feature
== Security
=== Password Hash Access Via Database Functions
By default on PostgreSQL, MySQL, and Microsoft SQL Server, Rodauth
uses database functions to access password hashes, with the user
running the application unable to get direct access to password
hashes. This reduces the risk of an attacker being able to access
password hashes and use them to attack other sites.
The rest of this section describes this feature in more detail, but
note that Rodauth does not require this feature be used and works
correctly without it. There may be cases where you cannot use
this feature, such as when using a different database or when you
do not have full control over the database you are using.
Passwords are hashed using bcrypt, and the password hashes are
kept in a separate table from the accounts table, with a foreign key
referencing the accounts table. Two database functions are added,
one to retrieve the salt for a password, and the other to check
if a given password hash matches the password hash for the user.
Two database accounts are used. The first is the account that the
application uses, which is referred to as the +app+ account. The +app+
account does not have access to read the password hashes. The other
account handles password hashes and is referred to as the +ph+
account. The +ph+ account sets up the database functions that can
retrieve the salt for a given account's password, and check if a
password hash matches for for a given account. The +ph+ account
sets these functions up so that the +app+ account can execute the
functions using the +ph+ account's permissions. This allows the
+app+ account to check passwords without having access to read
password hashes.
While the +app+ account is not be able to read password hashes, it
is still be able to insert password hashes, update passwords hashes,
and delete password hashes, so the additional security is not that
painful.
By disallowing the +app+ account access to the password hashes,
it is much more difficult for an attacker to access the password
hashes, even if they are able to exploit an SQL injection or remote
code execution vulnerability in the application.
The reason for extra security in regards to password hashes stems from
the fact that people tend to choose poor passwords and reuse passwords,
so a compromise of one database containing password hashes can result
in account access on other sites, making password hash storage of
critical importance even if the other data stored is not that important.
If you are storing other sensitive information in your database, you
should consider using a similar approach in other areas (or all areas)
of your application.
=== Tokens
Account verification, password resets, remember, and lockout tokens
all use a similar approach. They all provide a token, in the format
"account-id_long-random-string". By including the id of the account
in the token, an attacker can only attempt to bruteforce the token
for a single account, instead of being able to bruteforce tokens for
all accounts at once (which would be possible if the token was just a
random string).
Additionally, all comparisons of tokens use a timing-safe comparison
function to reduce the risk of timing attacks.
== PostgreSQL Database Setup
In order to get full advantages of Rodauth's security design on PostgreSQL,
multiple database accounts are involved:
1. database superuser account (usually postgres)
2. +app+ account (same name as application)
3. +ph+ account (application name with +_password+ appended)
The database superuser account is used to load extensions related to the
database. The application should never be run using the database
superuser account.
Note that there is not a simple way to use multiple database accounts in
the same PostgreSQL database on Heroku. You can still use Rodauth on
Heroku, it just won't have the same security benefits. That's not to say
it is insecure, just that it drops the security level for password hash
storage to the same level as other common authentication solutions.
=== Load extensions
If you want to use the login features for Rodauth, you need to load the
citext extension if you want to support case insensitive logins.
Example:
psql -U postgres -c "CREATE EXTENSION citext" ${DATABASE_NAME}
Note that on Heroku, this extension can be loaded using a standard database
account. If you don't want to support case sensitive logins, you don't
need to use the PostgreSQL citext extension. Just remember to modify the
migration below to use +String+ instead of +citext+ for the email.
=== Create database accounts
If you are currently running your application using the database superuser
account, the first thing you need to do is to create the +app+ database
account. It's often best to name this account the same as the
database name.
You should also create the +ph+ database account which will handle access
to the password hashes.
Example for PostgreSQL:
createuser -U postgres ${DATABASE_NAME}
createuser -U postgres ${DATABASE_NAME}_password
Note that if the database superuser account owns all of the items in the
database, you'll need to change the ownership to the database account you
just created. See https://gist.github.com/jeremyevans/8483320
for a way to do that.
== MySQL Database Setup
MySQL does not have the concept of object owners, and MySQL's GRANT/REVOKE
support is much more limited than PostgreSQL's. When using MySQL, it is
recommended to GRANT the +ph+ account ALL privileges on the database,
including the ability to GRANT permissions to the +app+ account:
CREATE USER '${DATABASE_NAME}'@'localhost' IDENTIFIED BY '${PASSWORD}';
CREATE USER '${DATABASE_NAME}_password'@'localhost' IDENTIFIED BY '${OTHER_PASSWORD}';
GRANT ALL ON ${DATABASE_NAME}.* TO '${DATABASE_NAME}_password'@'localhost' WITH GRANT OPTION;
You should run all migrations as the +ph+ account, and GRANT specific access
to the +app+ account as needed.
Adding the database functions on MySQL may require setting the
log_bin_trust_function_creators=1 setting in the MySQL configuration.
== Microsoft SQL Server Database Setup
Microsoft SQL Server has a concept of database owners, but similar to MySQL
usage it's recommended to use the +ph+ account as the superuser for the
database, and have it GRANT permissions to the +app+ account:
CREATE LOGIN rodauth_test WITH PASSWORD = 'rodauth_test';
CREATE LOGIN rodauth_test_password WITH PASSWORD = 'rodauth_test';
CREATE DATABASE rodauth_test;
USE rodauth_test;
CREATE USER rodauth_test FOR LOGIN rodauth_test;
GRANT CONNECT, EXECUTE TO rodauth_test;
EXECUTE sp_changedbowner 'rodauth_test_password';
You should run all migrations as the +ph+ account, and GRANT specific access
to the +app+ account as needed.
== Creating tables
Because two different database accounts are used, two different migrations
are required, one for each database account. Here are example migrations.
You can modify them to add support for additional columns, or remove tables
or columns related to features that you don't need.
First migration. On PostgreSQL, this should be run with the +app+ account,
on MySQL and Microsoft SQL Server this should be run with the +ph+ account.
Sequel.migration do
up do
extension :date_arithmetic
# Used by the account verification and close account features
create_table(:account_statuses) do
Integer :id, :primary_key=>true
String :name, :null=>false, :unique=>true
end
from(:account_statuses).import([:id, :name], [[1, 'Unverified'], [2, 'Verified'], [3, 'Closed']])
db = self
create_table(:accounts) do
primary_key :id, :type=>Bignum
foreign_key :status_id, :account_statuses, :null=>false, :default=>1
if db.database_type == :postgres
citext :email, :null=>false
constraint :valid_email, :email=>/^[^,;@ \r\n]+@[^,@; \r\n]+\.[^,@; \r\n]+$/
index :email, :unique=>true, :where=>{:status_id=>[1, 2]}
else
String :email, :null=>false
index :email, :unique=>true
end
end
deadline_opts = proc do |days|
if database_type == :mysql
{:null=>false}
else
{:null=>false, :default=>Sequel.date_add(Sequel::CURRENT_TIMESTAMP, :days=>days)}
end
end
# Used by the password reset feature
create_table(:account_password_reset_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[1]
end
# Used by the account verification feature
create_table(:account_verification_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
DateTime :requested_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# Used by the remember me feature
create_table(:account_remember_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[14]
end
# Used by the lockout feature
create_table(:account_login_failures) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
Integer :number, :null=>false, :default=>1
end
create_table(:account_lockouts) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
DateTime :deadline, deadline_opts[1]
end
# Used by the password expiration feature
create_table(:account_password_change_times) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
DateTime :changed_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# Used by the account expiration feature
create_table(:account_activity_times) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
DateTime :last_activity_at, :null=>false
DateTime :last_login_at, :null=>false
DateTime :expired_at
end
# Used by the single session feature
create_table(:account_session_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
end
# Used by the otp feature
create_table(:account_otp_keys) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :key, :null=>false
Integer :num_failures, :null=>false, :default=>0
Time :last_use, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
# Used by the recovery codes feature
create_table(:account_recovery_codes) do
foreign_key :id, :accounts, :type=>Bignum
String :code
primary_key [:id, :code]
end
# Used by the sms codes feature
create_table(:account_sms_codes) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :phone_number, :null=>false
Integer :num_failures
String :code
DateTime :code_issued_at, :null=>false, :default=>Sequel::CURRENT_TIMESTAMP
end
case database_type
when :postgres
user = get{Sequel.lit('current_user')} + '_password'
run "GRANT REFERENCES ON accounts TO #{user}"
when :mysql, :mssql
user = if database_type == :mysql
get{Sequel.lit('current_user')}.sub(/_password@/, '@')
else
get{DB_NAME{}}
end
run "GRANT ALL ON account_statuses TO #{user}"
run "GRANT ALL ON accounts TO #{user}"
run "GRANT ALL ON account_password_reset_keys TO #{user}"
run "GRANT ALL ON account_verification_keys TO #{user}"
run "GRANT ALL ON account_remember_keys TO #{user}"
run "GRANT ALL ON account_login_failures TO #{user}"
run "GRANT ALL ON account_lockouts TO #{user}"
run "GRANT ALL ON account_password_change_times TO #{user}"
run "GRANT ALL ON account_activity_times TO #{user}"
run "GRANT ALL ON account_session_keys TO #{user}"
run "GRANT ALL ON account_otp_keys TO #{user}"
run "GRANT ALL ON account_recovery_codes TO #{user}"
run "GRANT ALL ON account_sms_codes TO #{user}"
end
end
down do
drop_table(:account_sms_codes,
:account_recovery_codes,
:account_otp_keys,
:account_session_keys,
:account_activity_times,
:account_password_change_times,
:account_lockouts,
:account_login_failures,
:account_remember_keys,
:account_verification_keys,
:account_password_reset_keys,
:accounts,
:account_statuses)
end
end
Second migration, run using the +ph+ account:
require 'rodauth/migrations'
Sequel.migration do
up do
create_table(:account_password_hashes) do
foreign_key :id, :accounts, :primary_key=>true, :type=>Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_authentication_functions(self)
case database_type
when :postgres
user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
run "REVOKE ALL ON account_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT(id) ON account_password_hashes TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO #{user}"
when :mysql
user = get{Sequel.lit('current_user')}.sub(/_password@/, '@')
db_name = get{database{}}
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT (id) ON account_password_hashes TO #{user}"
when :mssql
user = get{DB_NAME{}}
run "GRANT EXECUTE ON rodauth_get_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_valid_password_hash TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_password_hashes TO #{user}"
run "GRANT SELECT ON account_password_hashes(id) TO #{user}"
end
# Used by the disallow_password_reuse feature
create_table(:account_previous_password_hashes) do
primary_key :id, :type=>Bignum
foreign_key :account_id, :accounts, :type=>Bignum
String :password_hash, :null=>false
end
Rodauth.create_database_previous_password_check_functions(self)
case database_type
when :postgres
user = get{Sequel.lit('current_user')}.sub(/_password\z/, '')
run "REVOKE ALL ON account_previous_password_hashes FROM public"
run "REVOKE ALL ON FUNCTION rodauth_get_previous_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_previous_password_hash_match(int8, text) FROM public"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT(id, account_id) ON account_previous_password_hashes TO #{user}"
run "GRANT USAGE ON account_previous_password_hashes_id_seq TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_get_previous_salt(int8) TO #{user}"
run "GRANT EXECUTE ON FUNCTION rodauth_previous_password_hash_match(int8, text) TO #{user}"
when :mysql
user = get{Sequel.lit('current_user')}.sub(/_password@/, '@')
db_name = get{database{}}
run "GRANT EXECUTE ON #{db_name}.* TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT (id, account_id) ON account_previous_password_hashes TO #{user}"
when :mssql
user = get{DB_NAME{}}
run "GRANT EXECUTE ON rodauth_get_previous_salt TO #{user}"
run "GRANT EXECUTE ON rodauth_previous_password_hash_match TO #{user}"
run "GRANT INSERT, UPDATE, DELETE ON account_previous_password_hashes TO #{user}"
run "GRANT SELECT ON account_previous_password_hashes(id, account_id) TO #{user}"
end
end
down do
Rodauth.drop_database_previous_password_check_functions(self)
Rodauth.drop_database_authentication_functions(self)
drop_table(:account_previous_password_hashes, :account_password_hashes)
end
end
To support multiple separate migration users, you can run the migration
for the password user using Sequel's migration API:
Sequel.extension :migration
Sequel.postgres('DATABASE_NAME', :user=>'PASSWORD_USER_NAME') do |db|
Sequel::Migrator.run(db, 'path/to/password_user/migrations', :table=>'schema_info_password')
end
If the database is not PostgreSQL, MySQL, or Microsoft SQL Server, or you
cannot use multiple user accounts, just combine the two migrations into a
single migration.
One thing to notice in the above migrations is that Rodauth uses additional
tables for additional features, instead of additional columns in a single
table.
== Usage
=== Basic Usage
Rodauth is a Roda plugin and loaded the same way other Roda plugins
are loaded:
plugin :rodauth do
end
The block passed to the plugin call uses the Rodauth configuration DSL.
The one configuration method that should always be used is +enable+,
which chooses which features you would like to load:
plugin :rodauth do
enable :login, :logout
end
Once features are loaded, you can use any of the configuration methods
supported by the features. There are two types of configuration
methods. The first type are called auth methods, and they take a
block which overrides the default method that Rodauth uses. Inside the
block, you can call super if you want to get the default behavior, though
you must provide explicit arguments to super. There is no need to
call super in before or after hooks, though. For example, if you want to
add additional logging when a user logs in:
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account.email} logged in!"
end
end
Inside the block, you are in the context of the Rodauth::Auth
instance related to the request. This object has access to everything
related to the request via methods:
request :: RodaRequest instance
response :: RodaResponse instance
scope :: Roda instance
session :: session hash
flash :: flash message hash
account :: account model instance (if set by an earlier Rodauth method)
So if you want to log the IP address for the user during login:
plugin :rodauth do
enable :login, :logout
after_login do
LOGGER.info "#{account.email} logged in from #{request.ip}"
end
end
The second type of configuration methods are called auth value
methods. They are similar to auth methods, but instead of just
accepting a block, they can optionally accept a single argument
without a block, which will be treated as a block that just returns
that value. For example, the accounts_table method sets the database
table storing accounts, so to override it, you can call the method
with a symbol for the table:
plugin :rodauth do
enable :login, :logout
accounts_table :users
end
Note that all auth value methods can still take a block, allowing
overriding for all behavior, using any information from the request:
plugin :rodauth do
enable :login, :logout
accounts_table do
request.ip.start_with?("192.168.1") ? :admins : :users
end
end
By allowing every configuration method to take a block, Rodauth
should be flexible enough to integrate into most legacy systems.
=== Feature Documentation
The options/methods for the supported features are listed on a
separate page per feature. If these links are not active, please
view the appropriate file in the doc directory.
* {Base}[rdoc-ref:doc/base.rdoc] (this feature is autoloaded)
* {Login Password Requirements Base}[rdoc-ref:doc/login_password_requirements_base.rdoc] (this feature is autoloaded by features that set logins/passwords)
* {Email Base}[rdoc-ref:doc/email_base.rdoc] (this feature is autoloaded by features that send email)
* {Two Factor Base}[rdoc-ref:doc/two_factor_base.rdoc] (this feature is autoloaded by 2 factor authentication features)
* {Login}[rdoc-ref:doc/login.rdoc]
* {Logout}[rdoc-ref:doc/logout.rdoc]
* {Change Password}[rdoc-ref:doc/change_password.rdoc]
* {Change Login}[rdoc-ref:doc/change_login.rdoc]
* {Reset Password}[rdoc-ref:doc/reset_password.rdoc]
* {Create Account}[rdoc-ref:doc/create_account.rdoc]
* {Close Account}[rdoc-ref:doc/close_account.rdoc]
* {Verify Account}[rdoc-ref:doc/verify_account.rdoc]
* {Confirm Password}[rdoc-ref:doc/confirm_password.rdoc]
* {Remember}[rdoc-ref:doc/remember.rdoc]
* {Lockout}[rdoc-ref:doc/lockout.rdoc]
* {OTP}[rdoc-ref:doc/otp.rdoc]
* {Recovery Codes}[rdoc-ref:doc/recovery_codes.rdoc]
* {SMS Codes}[rdoc-ref:doc/sms_codes.rdoc]
* {Verify Change Login}[rdoc-ref:doc/verify_change_login.rdoc]
* {Verify Account Grace Period}[rdoc-ref:doc/verify_account_grace_period.rdoc]
* {Password Grace Period}[rdoc-ref:doc/password_grace_period.rdoc]
* {Password Complexity}[rdoc-ref:doc/password_complexity.rdoc]
* {Disallow Password Reuse}[rdoc-ref:doc/disallow_password_reuse.rdoc]
* {Password Expiration}[rdoc-ref:doc/password_expiration.rdoc]
* {Account Expiration}[rdoc-ref:doc/account_expiration.rdoc]
* {Session Expiration}[rdoc-ref:doc/session_expiration.rdoc]
* {Single Session}[rdoc-ref:doc/single_session.rdoc]
* {JWT}[rdoc-ref:doc/jwt.rdoc]
=== Calling Rodauth in the Routing Tree
In general, you will usually want to call rodauth early in your
route block:
route do |r|
r.rodauth
# ...
end
Note that will allow Rodauth to run, but it won't force people
to login or add any security to your site. If you want to force
all users to login, you need to redirect to them login page if
they are not already logged in:
route do |r|
r.rodauth
rodauth.require_authentication
# ...
end
If only certain parts of your site require logins, then you can
only redirect if they are not logged in certain branches of the
routing tree:
route do |r|
r.rodauth
r.on "admin" do
rodauth.require_authentication
# ...
end
# ...
end
In some cases you may want to have rodauth run inside a branch of
the routing tree, instead of in the root. You can do this by
setting a +:prefix+ when configuring Rodauth, and calling +r.rodauth+
inside a matching routing tree branch:
plugin :rodauth do
enable :login, :logout
prefix "auth"
end
route do |r|
r.on "auth" do
r.rodauth
end
rodauth.require_authentication
# ...
end
=== +rodauth+ Methods
Most of Rodauth's functionality is exposed via +r.rodauth+, which allows
Rodauth to handle routes for the features you have enabled (such as +/login+
for login). However, as you have seen above, you may want to call methods on
the +rodauth+ object, such as for checking if the current request has been
authenticated.
Here are methods designed to be callable on the +rodauth+ object outside
+r.rodauth+:
require_login :: Require the session be logged in, redirecting the request to the
login page if the request has not been logged in.
require_authentication :: Similar to +require_login+, but also requires
two factor authentication if the account has setup
two factor authentication. Redirects the request to
the two factor authentication page if logged in but not
authenticated via two factors.
logged_in? :: Whether the session has been logged in.
authenticated? :: Similar to +logged_in?+, but if the account has setup two
factor authentication, whether the session has authenticated
via two factors.
require_two_factor_setup :: (two_factor_base feature) Require the session to have
setup two factor authentication, redirecting the
request to the two factor authentication setup page
if not.
uses_two_factor_authentication? :: (two_factor_base feature) Whether the account
for the current session has setup two factor
authentication.
update_last_activity :: (account_expiration feature) Update the last activity
time for the current account. Only makes sense to use
this if you are expiring accounts based on last activity.
require_current_password :: (password_expiration feature) Require a current
password, redirecting the request to the change
password page if the password for the account has
expired.
load_memory :: (remember feature) If the session has not been authenticated, look
for the remember cookie. If present and valid, automatically
log the session in, but mark that it was logged in via a remember
key.
logged_in_via_remember_key? :: (remember feature) Whether the current session has
been logged in via a remember key. For security
sensitive actions where you want to require the user
to reenter the password, you can use the
confirm_password feature.
check_session_expiration :: (session_expiration feature) Check whether the current
session has expired, automatically logging the session
out if so.
check_single_session :: (single_session expiration) Check whether the current
session is still the only valid session, automatically logging
the session out if not.
verified_account? :: (verify_grace_period extension) Whether the account is currently
verified. If false, it is because the account is allowed to
login as they are in the grace period.
locked_out? :: (lockout feature) Whether the account for the current session has been
locked out.
=== With Multiple Configurations
Rodauth supports using multiple rodauth configurations in the same
application. You just need to load the plugin a second time,
providing a name for any alternate configuration:
plugin :rodauth do
end
plugin :rodauth, :name=>:secondary do
end
Then in your routing code, any time you call rodauth, you can provide
the name as an argument to use that configuration:
route do |r|
r.on 'secondary' do
r.rodauth(:secondary)
end
r.rodauth
end
=== With Password Hashes Inside the Accounts Table
You can use Rodauth if you are storing password hashes in the same
table as the accounts. You just need to specify which column
stores the password hash:
plugin :rodauth do
account_password_hash_column :password_hash
end
When this option is set, Rodauth will do the password hash check
in ruby.
=== When Using PostgreSQL/MySQL/Microsoft SQL Server without Database Functions
If you want to use Rodauth on PostgreSQL, MySQL, or Microsoft SQL Server
without using database functions for authentication, but still storing password
hashes in a separate table, you can do so:
plugin :rodauth do
use_database_authentication_functions? false
end
Conversely, if you implement the rodauth_get_salt and
rodauth_valid_password_hash functions on a database that isn't
PostgreSQL, MySQL, or Microsoft SQL Server, you can set this value to true.
=== With Custom Authentication (such as LDAP)
You can use Rodauth with other authentication types, by overriding
a single configuration setting. For example, if you have accounts
stored in the database, but authentication happens via LDAP, you
can use the +simple_ldap_authenticator+ library:
require 'simple_ldap_authenticator'
plugin :rodauth do
enable :login, :logout
require_bcrypt? false
password_match? do |password|
SimpleLdapAuthenticator.valid?(account.username, password)
end
end
If you aren't storing accounts in the database, but want to allow
any valid LDAP user to login, you can do something like this:
require 'simple_ldap_authenticator'
plugin :rodauth do
enable :login, :logout
# Don't require the bcrypt library, since using LDAP for auth
require_bcrypt? false
# Treat the login itself as the account
account_from_login{|l| l.to_s}
# Use the login provided as the session value
account_session_value{account}
# Store session value in :login key, since the :account_id
# default wouldn't make sense
session_key :login
password_match? do |password|
SimpleLdapAuthenticator.valid?(account, password)
end
end
Note that when using custom authentication, using some of Rodauth's
features such as change login and change password either would not
make sense or would require some additional custom configuration.
The login and logout features should work correctly with the examples
above, though.
=== With Other Web Frameworks
You can use Rodauth even if your application does not use the Roda web
framework. This is possible by adding a Roda middleware that uses
Rodauth:
require 'roda'
class RodauthApp < Roda
plugin :middleware
plugin :rodauth do
enable :login
end
route do |r|
r.rodauth
rodauth.require_authentication
env['rodauth'] = rodauth
end
end
use RodauthApp
Note that Rodauth expects the Roda app it is used in to provide a
layout. So if you are using Rodauth as middleware for another app,
if you don't have a +views/layout.erb+ file that Rodauth can use,
you should probably also add load Roda's +render+ plugin
with the appropriate settings that allow Rodauth to use the same
layout as the application.
By setting env['rodauth'] = rodauth in the route block
inside the middleware, you can easily provide a way for your
application to call Rodauth methods.
For an example of integrating Rodauth into a real application that
doesn't use Roda, see
{this example integrating Rodauth into Ginatra, a Sinatra-based git repository viewer}[https://github.com/jeremyevans/ginatra/commit/28108ebec96e8d42596ee55b01c3f7b50c155dd1].
To see an example of integrating Rodauth into a Rails application, see
{this example porting Rodauth's demo site to Rails}[https://github.com/jeremyevans/rodauth-demo-rails].
This uses the {roda-rails gem}[https://github.com/jeremyevans/roda-rails]
so that Rodauth uses Rails' CSRF and flash support.
=== Using 2 Factor Authentication
Rodauth ships with 2 factor authentication support via TOTP (Time-Based
One-Time Passwords, RFC 6238). There are a wide variety of ways in
which to integrate 2 factor authentication into your site with Rodauth,
based on the needs of the application.
The 2 factor authentication support is part of the OTP feature, which
needs to be enabled in addition to the login feature. In addition,
when implementing 2 factor authentication, you should generally
provide a backup 2nd factor if the primary second factor is not
available. Rodauth supports SMS codes and recovery codes as other
2nd factors.
If you want to support but not require 2 factor authentication:
plugin :rodauth do
enable :login, :logout, :otp, :recovery_codes, :sms_codes
end
route do |r|
r.rodauth
rodauth.require_authentication
# ...
end
If you want to force all users to use OTP authentication, requiring users
that don't currently have an account to set one up:
route do |r|
r.rodauth
rodauth.require_authentication
rodauth.require_two_factor_authentication_setup
# ...
end
Similarly to requiring authentication in general, it's possible to require
login authentication for most of the site, but require 2 factor
authentication only for particular branches:
route do |r|
r.rodauth
rodauth.require_login
r.on "admin" do
rodauth.require_two_factor_authenticated
end
# ...
end
== JSON API Support
To add support for handling JSON responses, you can pass the +:json+
option to the plugin, and enable the JWT feature in addition to
other features you plan to use:
plugin :rodauth, :json=>true do
enable :login, :logout, :jwt
end
If you do not want to load the HTML plugins that Rodauth usually loads
(render, csrf, flash, h), because you are building a JSON-only API,
pass :json => :only
plugin :rodauth, :json=>:only do
enable :login, :logout, :jwt
end
Note that by default, the features that send email depend on the
render plugin, so if using the :json=>:only option, you
either need to load the render plugin manually or you need to
use the necessary *_email_body configuration options to specify
the body of the emails.
The JWT feature enables JSON API support for all of the other features
that Rodauth ships with.
=== Adding Custom Methods to the +rodauth+ Object
Inside the configuration block, you can use +auth_class_eval+ to add
custom methods that will be callable on the +rodauth+ object.
plugin :rodauth do
enable :login
auth_class_eval do
def require_admin
request.redirect("/") unless account[:admin]
end
end
end
route do |r|
r.rodauth
r.on "admin"
rodauth.require_admin
end
end
=== Using External Features
The enable configuration method is able to load features external to
Rodauth. You need to place the external feature file where it can be
required via rodauth/features/feature_name. That file should
use the following basic structure
module Rodauth
# :feature_name will be the argument given to enable to
# load the feature
FeatureName = Feature.define(:feature_name) do
# Shortcut for defining auth value methods with static values
auth_value_method :method_name, 1 # method_value
auth_value_methods # one argument per auth value method
auth_methods # one argument per auth method
route do |r|
# This block is taken for requests to the feature's route.
# This block is evaluated in the scope of the Rodauth::Auth instance.
# r is the Roda::RodaRequest instance for the request
r.get do
end
r.post do
end
end
configuration_eval do
# define additional configuration specific methods here, if any
end
# define the default behavior for the auth_methods
# and auth_value_methods
# ...
end
end
See the source code for the features that ship with Rodauth for an
example of how to construct features.
=== Overriding Route-Level Behavior
All of Rodauth's configuration methods change the behavior of the
Rodauth::Auth instance. However, in some cases you may want to
overriding handling at the routing layer. You can do this easily
by adding an appropriate route before calling +r.rodauth+:
route do |r|
r.post 'login' do
# Custom POST /login handling here
end
r.rodauth
end
== Upgrading from 0.9.x
To upgrade from 0.9.x to the current version, if you were using
the account_valid_password database function, you need to drop
it and add the two database functions listed in the migration
section above. You can add the following code to a migration to
accomplish that:
require 'rodauth/migrations'
run "DROP FUNCTION account_valid_password(int8, text);"
Rodauth.create_database_authentication_functions(self)
run "REVOKE ALL ON FUNCTION rodauth_get_salt(int8) FROM public"
run "REVOKE ALL ON FUNCTION rodauth_valid_password_hash(int8, text) FROM public"
run "GRANT EXECUTE ON FUNCTION rodauth_get_salt(int8) TO ${DATABASE_NAME}"
run "GRANT EXECUTE ON FUNCTION rodauth_valid_password_hash(int8, text) TO ${DATABASE_NAME}"
== Similar Projects
All of these are Rails-specific:
* Devise
* Authlogic
* Sorcery
== Author
Jeremy Evans