Using Tachiban with Hanami
In this post I’ll describe how I use the authentication gem Tachiban in my Hanami 1.3 applications. I use a separate Hanami application to handle authentication and authorization. I’ll focus on the authorization (Rokku) in a separate post.
Authentication application elements
There are currently five main application elements that drive authentication: users, user sessions, dashboard, password reset/update and setting Tachiban defaults. I use one separate module for setting the Tachiban defaults, while I override certain Tachiban methods where appropriate.
1. Users
1.1 Entities
The user attributes are defined as follows to make use of Tachiban as per prerequisites defined here.
Hanami::Model.migration do
change do
create_table :users do
primary_key :id
column :created_at, DateTime, null: false
column :updated_at, DateTime, null: false
column :name, String, null: false
column :surname, String, null: false
column :username, String, null: false
column :email, String, null: false, unique: true
column :password_reset_sent_at, DateTime
column :hashed_pass, String, null: false
column :token, String
column :role, String, null: false
column :active_status, TrueClass, null: false
end
end
end
1.2 Controllers
The most relevant action for Tachiban is Create
. The translate_err_mess
method in the sample code below is a custom method for translating error messages. I need this to adjust translations for Slovenian, for example.
The most important thing is to setup a hashed password to be saved in the database.
module Users
class Create
include AuthApp::Action
params do
configure do
config.messages = :i18n
end
required(:user).schema do
required(:name).filled(:str?, min_size?: 3)
required(:surname).filled(:str?, min_size?: 3)
required(:username).filled(:str?, min_size?: 3)
required(:email).filled(format?: /\A[^@]*?@[\w\-\.]*?\.\w*\z/)
required(:password).filled(size?: 8..32)
end
end
def call(params)
if params.valid?
begin
password = params[:user][:password]
params[:user][:hashed_pass] = hashed_password(password)
params[:user][:role] = 'new_user'
params[:user][:active_status] = true
UserRepository.new.create(params[:user])
redirect_to routes.users_path
rescue StandardError => e
@standard_error = e
self.status = 422
redirect_to routes.new_user_path
end
else
@params_errors = translate_err_mess(params.errors)
self.status = 422
redirect_to routes.new_user_path
end
end
end
end
1.3 Routes
resources :users
2. User sessions
2.1 Entities
There are no entities required for user sessions, but only controllers and actions.
2.2 Controllers
The three actions for the user sessions controller are:
New
Create
andDestroy
.
2.2.1 New
In the New
action I first set the current user to nil and also override the methods that check for the logged in user and handle session. This is needed in order to prevent an infinite loop of checking and redirecting.
module AuthApp
module Controllers
module UserSessions
class New
include AuthApp::Action
def call(params)
session[:current_user] = nil
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
end
end
2.2.2 Create
The Create
action finds the user trying to log in and if they are authenticated they are logged in. Otherwise the logout
method is called. The check_for_logged_in_user
and handle_session
are overridden again.
module AuthApp
module Controllers
module UserSessions
class Create
include AuthApp::Action
params do
required(:user_session).schema do
required(:email).filled(format?: /\A[^@]*?@[\w\-\.]*?\.\w*\z/)
required(:password).filled(size?: 8..64)
end
end
def call(params)
@user = UserRepository.new.find_by_email(params[:user_session][:email])
if authenticated?(params[:user_session][:password])
login
else
logout
end
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
end
end
2.2.3 Destroy
Lastly, the Destroy
action logs the user out. This acion also requires bypassing the Tachiban’s checking methods.
module AuthApp
module Controllers
module UserSessions
class Destroy
include AuthApp::Action
def call(params)
logout
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
end
end
2.3 Routes
resources :user_sessions, only: [:create]
get '/login', to: 'user_sessions#new'
get '/logout', to: 'user_sessions#destroy'
3. Dashboard
The dashboard controller is basically a home page for the app and thus not needed for the Tachibam implementation. I include it here to better ilustrate the entire app.
Routes
root to: 'dashboard#index'
get '/dashboard', to: 'dashboard#index'
4. Password reset/update
4.1 Entities
There are no entities required for user sessions, but only controllers and actions. For both I use the EDIT/UPDATE actions. The templates are not included here.
4.2 Controllers
There are two set of actions for this functionality: one for requesting the password reset (or to handle a forgotten password) and the other for updating the password.
4.2.1 Password reset UPDATE action
module AuthApp
module Controllers
module Passwordreset
class Update
include AuthApp::Action
params do
#specify and validate params
end
def call(params)
if params.valid?
email = params[:passwordreset][:email]
user_repo = UserRepository.new
user = user_repo.new.find_by_email(email)
# Generate the token and update user with the time of the password reset link
# and the generated token.
token = SecureRandom.urlsafe_base64
password_reset_sent_time = Time.now
user_repo.update(user.id, password_reset_token: token, password_reset_sent_at: password_reset_sent_time)
# Set the reset e-mail title and body
title = "Password reset"
body = "http://localhost:2300/authapp/passwordupdate/#{token}"
# Send the reset email
Mailers::Passwordreset.deliver(mail_title: title, mail_body: body, user_email: email)
flash[:success_notice] = "Password reset link sent."
redirect_to '/'
else
#
#
#
end
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
4.2.2 Password update EDIT action
module AuthApp
module Controllers
module Passwordupdate
class Edit
include AuthApp::Action
expose :url_token
def call(params)
@url_token = params[:token]
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
end
end
4.2.3 Password update UPDATE action
module AuthApp
module Controllers
module Passwordupdate
class Update
include AuthApp::Action
params do
required(:token).filled
required(:passwordupdate).schema do
required(:new_password).filled
required(:repeat_password).filled
end
end
def call(params)
if params.valid?
new_pass = params[:passwordupdate][:new_password]
token = params[:token]
# Find user by token.
user_repo = UserRepository.new
@user = user_repo.find_by_token(token)
I18n.t :sn_passwordupdate_link_validity_expired
# Check for reset link validity
if password_reset_url_valid?(7200)
flash[:failed_notice] = I18n.t :sn_passwordupdate_link_validity_expired
redirect_to routes.root_path
else
new_hashed_pass = hashed_password(new_pass)
user_repo.update(@user.id, hashed_pass: new_hashed_pass)
flash[:success_notice] = I18n.t :sn_passwordupdate_password_reset
redirect_to routes.root_path
end
else
self.status = 422
flash[:error_messages] = translate_err_mess(params.errors)
redirect_to routes.root_path
end
end
private
def check_for_logged_in_user; end
def handle_session; end
end
end
end
end
4.3 Mailers
class Mailers::Passwordreset
include Hanami::Mailer
from 'my-app@some-domain.com'
to :recipient
subject :mail_title
private
def emailbody
mail_body
end
def recipient
user_email
end
end
Also don’t forget to setup the delivery configuration in the environment.rb
.
4.4 Routes
In order for the code below to work these routes have to be defined:
resource :passwordreset, only: [:edit, :update]
get '/passwordupdate/:token', token: /([^\/]+)/, to: 'passwordupdate#edit'
patch '/passwordupdate/:token', token: /([^\/]+)/, to: 'passwordupdate#update'
5. Setting Tachiban defaults
In Authentication
module I do the following.
- Specify Tachiban’s methods that I want to call in the before block for every action.
- Set default urls for my application.
module AuthApp
module Authentication
def self.included(action)
action.class_eval do
before :handle_session_redirect_url, :logout_redirect_url, :login_redirect_url, :check_for_logged_in_user, :handle_session
end
end
private
def handle_session_redirect_url
@redirect_url = '/authapp/login'
end
def logout_redirect_url
@logout_redirect_url = '/authapp/login'
end
def login_redirect_url
@login_redirect_url = '/user_home'
end
end
end
Don’t forget to include this module in the application.rb
.
#
#
controller.prepare do
include AuthApp::Authentication
# include MyAuthentication # included in all the actions
# before :authenticate! # run an authentication before callback
end
#
#