JWT Token-based custom user authentication for Rails API only (Part 03)
Sulman Baig
Posted on May 28, 2021
The code available at:
sulmanweb / rails-api-user-custom-auth
Rails API user custom authentication using JWT project
This part emphasis on sessions create/destroy services with email confirmation and reset password services of the user custom JWT auth.
Sessions Controller:
Now that we have created all the required JWT libraries required for authentication, we will move fast and create three services of sign in, sign out, and validate token.
Create a file app/controllers/auth/sessions_controller.rb
with following data:
class Auth::SessionsController < ApplicationController
include CreateSession
before_action :authenticate_user, only: [:validate_token, :destroy]
def create
return error_insufficient_params unless params[:email].present? && params[:password].present?
@user = User.find_by(email: params[:email])
if @user
if @user.authenticate(params[:password])
@token = jwt_session_create @user.id
if @token
@token = "Bearer #{@token}"
return success_session_created
else
return error_token_create
end
else
return error_invalid_credentials
end
else
return error_invalid_credentials
end
end
def validate_token
@token = request.headers['Authorization']
@user = current_user
success_valid_token
end
def destroy
headers = request.headers['Authorization'].split(' ').last
session = Session.find_by(token: JsonWebToken.decode(headers)[:token])
session.close
success_session_destroy
end
protected
def success_session_created
response.headers['Authorization'] = "Bearer #{@token}"
render status: :created, template: "auth/auth"
end
def success_valid_token
response.headers['Authorization'] = "Bearer #{@token}"
render status: :ok, template: "auth/auth"
end
def success_session_destroy
render status: :no_content, json: {}
end
def error_invalid_credentials
render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.auth.invalid_credentials')]}
end
def error_token_create
render status: :unprocessable_entity, json: {errors: [I18n.t('errors.controllers.auth.token_not_created')]}
end
def error_insufficient_params
render status: :unprocessable_entity, json: {errors: [I18n.t('errors.controllers.insufficient_params')]}
end
end
The authenticate
method in line 10 is provided by bcrypt gem we installed before. This method converts the provided password to a hash and matches that hash to a hashed password saved in database.
The validate_token
method is an extra method can be used by a logged-in user coming after sometime. This method return 401 if token is expired or incorrect otherwise return the user data which may be updated through another active session.
Test these services by adding to config/routes.rb
in auth
namespace:
post "sign_in", to: "sessions#create"
get "validate_token", to: "sessions#validate_token"
delete "sign_out", to: "sessions#destroy"
User Email Confirmation:
Now we create user email confirmation system. In user model app/models/user.rb
a method for sending email to confirm its account.
def send_confirm_email
unless confirmed?
verification = UserVerification.create(user_id: id, verify_type: :confirm_email)
url = Rails.application.routes.url_helpers.auth_confirm_email_url(host: "localhost:3000", token: verification.token)
# ADD Email Job with `url` added in "CONFIRM EMAIL" button
end
end
Also call above method in user after create callback so that whenever user is created send confirmation email:
after_create :send_confirm_email
Create Email job using API or Action Mailer SMTP system for actually sending the email. Docs are here: https://guides.rubyonrails.org/action_mailer_basics.html
Now create controller for user confirmation services at app/controllers/auth/confirmations_controller.rb
with following code:
class Auth::ConfirmationsController < ApplicationController
include CreateSession
before_action :authenticate_user, only: :resend_confirm_email
def confirm_email
return error_insufficient_params unless params[:token]
verification = UserVerification.search(:pending, :confirm_email, params[:token])
return error_invalid_token if verification.nil?
if (verification.created_at + UserVerification::TOKEN_LIFETIME) > Time.now
verification.user.confirm
verification.update(status: :done)
@token = jwt_session_create verification.user_id
# Redirect to the page that says the email is confirmed successfully or can be redirected to the app
redirect_to "#{ENV['REDIRECT_CONFIRM_EMAIL']}?token=#{@token}"
else
error_confirm_email_late
end
end
def resend_confirm_email
current_user.send_confirm_email
success_resend_confirm_email
end
protected
def success_resend_confirm_email
render status: :ok, json: {message: I18n.t('messages.resend_confirm_email')}
end
def error_insufficient_params
render status: :unprocessable_entity, json: {errors: [I18n.t('errors.controllers.insufficient_params')]}
end
def error_confirm_email_late
render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.verifications.late')]}
end
def error_invalid_token
render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.verifications.invalid_token')]}
end
end
The confirm_email
method gets the query param of token of user verification received in email, verifies that token, and then redirects to the page that says ‘Your email is verified successfully’.
The resend_confirm_email
method can be used to nag a logged in user if not confirmed its email.
Add these two methods in routes auth namespace:
get "confirm_email", to: "confirmations#confirm_email"
put "resend_confirm_email", to: "confirmations#resend_confirm_email"
Forgot and Change User Password:
Similar to confirmation email system we did above we create a forgot password email sending method is user model app/models/user.rb
:
def send_reset_email
if confirmed?
verification = UserVerification.create(user_id: id, verify_type: :reset_email)
url = Rails.application.routes.url_helpers.auth_verify_reset_password_email_url(host: "localhost:3000", token: verification.token)
# ADD Email Job with `url` added in "RESET YOUR EMAIL" button
end
end
Now create passwords controller app/controllers/auth/passwords_controller.rb
:
class Auth::PasswordsController < ApplicationController
include CreateSession
before_action :authenticate_user, only: [:reset_password]
def create_reset_email
return error_insufficient_params unless params[:email].present?
user = User.find_by(email: params[:email])
user.send_reset_email unless user.nil?
success_send_reset_email
end
def verify_reset_email_token
return error_insufficient_params unless params[:token]
verification = UserVerification.search(:pending, :reset_email, params[:token])
return error_invalid_token if verification.nil?
if (verification.created_at + UserVerification::TOKEN_LIFETIME) > Time.now
verification.update(status: :done)
verification.user.confirm unless verification.user.confirmed?
token = jwt_session_create verification.user_id
# Redirect to the page where a logged in user can change its password
redirect_to "#{ENV['REDIRECT_RESET_EMAIL']}?token=#{token}"
else
error_reset_email_late
end
end
def reset_password
@user = current_user
return error_insufficient_params unless params[:password].present? && params[:confirm_password].present?
return error_password_mismatch if params[:password] != params[:confirm_password]
if @user.update(password: params[:password])
return success_password_reset
else
return error_user_save
end
end
protected
def error_insufficient_params
render status: :unprocessable_entity, json: {errors: [I18n.t('errors.controllers.insufficient_params')]}
end
def success_send_reset_email
render status: :created, json: {message: I18n.t('messages.reset_password_email_sent')}
end
def success_password_reset
render status: :ok, json: {message: I18n.t('messages.email_reset_success')}
end
def error_reset_email_late
render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.verifications.late')]}
end
def error_invalid_token
render status: :unauthorized, json: {errors: [I18n.t('errors.controllers.verifications.invalid_token')]}
end
def error_user_save
render status: :unprocessable_entity, json: {errors: @user.errors.full_messages}
end
def error_password_mismatch
render status: :unprocessable_entity, json: {errors: [I18n.t('errors.controllers.auth.password_mismatch')]}
end
end
create_reset_email
that takes email in body and if a user with the email given exists in the system then sends an email with user verification token.
verify_reset_email_token
method is called when the reset password link is clicked in the forgot password email. This method verifies the token sent in email and creates a user session and redirects to a page where logged-in user can change their password.
reset_password
method is for logged-in user whether coming through proper session or forgot password verification session. This method takes the new password for the user to change its account password.
Now add these services to config/routes.rb
file in auth
namespace:
post "forgot_password_email", to: "passwords#create_reset_email"
get "verify_reset_password_email", to: "passwords#verify_reset_email_token"
put "reset_password", to: "passwords#reset_password"
Now our custom user authentication system is complete with the following routes:
auth_sign_up POST /auth/sign_up(.:format) auth/registrations#create
auth_destroy DELETE /auth/destroy(.:format) auth/registrations#destroy
auth_sign_in POST /auth/sign_in(.:format) auth/sessions#create
auth_validate_token GET /auth/validate_token(.:format) auth/sessions#validate_token
auth_sign_out DELETE /auth/sign_out(.:format) auth/sessions#destroy
auth_confirm_email GET /auth/confirm_email(.:format) auth/confirmations#confirm_email
auth_resend_confirm_email PUT /auth/resend_confirm_email(.:format) auth/confirmations#resend_confirm_email
auth_forgot_password_email POST /auth/forgot_password_email(.:format) auth/passwords#create_reset_email
auth_verify_reset_password_email GET /auth/verify_reset_password_email(.:format) auth/passwords#verify_reset_email_token
auth_reset_password PUT /auth/reset_password(.:format) auth/passwords#reset_password
Final code of all three parts is at:
sulmanweb / rails-api-user-custom-auth
Rails API user custom authentication using JWT project
Conclusion:
Custom user authentication gives us full control over the code so much that if the system requires the authenticating entity to be only mobile number not email then that is also possible. We can also change any scenario required for the project and also extra table and data is not present in database.
Happy Coding!
Posted on May 28, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.