I wanted to learn about the workings of an authenitaction system for a web application and I decided to make my own for a Hanami based web application I was working on at the time.
I’ve exctracted the code and system to a separate gem Tachiban .
Below I’ll try to retrack the steps in the process of setting up my own the auth system.
1. Preparing the user entity
1.1 The entity
class User
include Hanami :: Entity
attributes :name , :surname , :email , :password , :password_hash , :password_salt ,
:password_confirmation , :password_reset_token , :password_reset_sent_at , :active_status ,
:username
end
1.2 The mapping
collection :users do
entity User
repository UserRepository
attribute :id , Integer
attribute :name , String
attribute :surname , String
attribute :email , String
attribute :password_hash , String
attribute :password_salt , String
attribute :password_reset_token , String
attribute :password_reset_sent_at , DateTime
attribute :active_status , String
attribute :username , String
end
1.3 The create action for the user entity
I use [[bcrypt
https://rubygems.org/gems/bcrypt/versions/3.1.11]] gem for password encryption, so prior to user creation we need to setup the password salt and hash.
password_salt = BCrypt :: Engine . generate_salt
password_hash = BCrypt :: Engine . hash_secret ( params [ :user ][ :password ], password_salt )
Then we create the user:
@user = UserRepository . create ( User . new ( name: name , surname: surname , email: email , password_hash: password_hash , password_salt: password_salt , active_status: active_status , username: username ))
2. Creating the session and logging the user in
module Web::Controllers::UserSessions
class Create
include Web :: Action
params do
## specify and validate params
end
end
def call ( params )
if params . valid?
email = params [ :user_session ][ :email ]
password = params [ :user_session ][ :password ]
# User is looked up in the repository by email
@user = UserRepository . user_with_email ( email )
# Email and password (ad hoc encrypted) are
#compared with those in the database and
#corresponding control flow is in place.
if ! @user
session [ :current_user ] = nil
flash [ :failed_notice ] = "The user account
doesn't exist."
redirect_to '/'
else
if @user && @user . password_hash ==
BCrypt :: Engine . hash_secret ( password ,
@user . password_salt )
session [ :current_user ] = @user
flash [ :success_notice ] = "You are now logged in."
redirect_to '/user_home'
else
session [ :current_user ] = nil
flash [ :failed_notice ] = "Your log in data
was incorrect."
redirect_to '/'
end
end
else
#
#
end
end
end
end
3. Authentication methods
I’ve setup some methods in an authentication module that can be run before actions. The code below is setup for development mode so the test clauses are still there.
def check_for_user_class
unless ENV [ 'HANAMI_ENV' ] == 'test'
halt 401 unless session [ :current_user ]. class == User
end
end
def check_for_admin_role
unless ENV [ 'HANAMI_ENV' ] == 'test'
halt 401 unless session [ :current_user ]. role == "administrator"
end
end
def check_for_signed_in_user
unless ENV [ 'HANAMI_ENV' ] == 'test'
halt 401 unless session [ :current_user ]
end
end
4. Logout
module Web::Controllers::Logout
class Index
include Web :: Action
# Logs the current user out by setting its session value to nil.
def call ( _ )
session [ :current_user ] = nil
flash [ :success_notice ] = "You've been logged out."
redirect_to "/"
end
end
end
5. Password reset
5.1 Password reset link request action
In order for the code below to work these routes have to be defined:
get '/passwordupdate/:token' , token: /([^\/]+)/ , to: 'passwordupdate#edit'
patch '/passwordupdate/:token' , token: /([^\/]+)/ , to: 'passwordupdate#update'
module Web::Controllers::Passwordreset
class Update
include Web :: Action
params do
#specify and validate params
end
def call ( params )
if params . valid?
# Sets variables from param values
email = params [ :passwordreset ][ :email ]
user = UserRepository . user_with_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 . update ( password_reset_token: token , password_reset_sent_at: password_reset_sent_time )
user = repository . update ( user )
# Set the reset e-mail tiel and body
title = "Ponastavitev gesla"
body = "http://localhost:2300/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
flash [ :failed_notice ] = "The e-mail wasn't found."
redirect_to 'passwordreset/edit'
end
end
end
end
5.2 Password update action
module Web::Controllers::Passwordupdate
class Update
include Web :: Action
params do
param :token , presence: true
param :passwordupdate do
param :new_password , presence: true
param :repeat_password , presence: true
end
end
def call ( params )
if params . valid?
@new_pass = params [ :passwordupdate ][ :new_password ]
token = params [ :token ]
# Find user by token, looking at both repositories.
@user = UserRepository . user_by_token ( token )
@repository = UserRepository
# Check for reset link validity
if Time . now > @user . password_reset_sent_at . to_time + 7200
flash [ :failed_notice ] = "Reset link validity (2 hours) has expired."
redirect_to "/"
else
password_salt = BCrypt :: Engine . generate_salt
password_hash = BCrypt :: Engine . hash_secret ( @new_pass , password_salt )
@user . update ( password_hash: password_hash , password_salt: password_salt )
@user = @repository . update ( @user )
flash [ :success_notice ] = "The password was reset."
redirect_to "/"
end
else
#
end
end
end
end
Post a comment: