DEV Community

Cover image for JWT Token-based custom user authentication for Rails API only (Part 03)
Sulman Baig
Sulman Baig

Posted on • Originally published at sulmanweb.com

JWT Token-based custom user authentication for Rails API only (Part 03)

The code available at:

GitHub logo 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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

Sign In Service

Validate Token Service

Sign Out Service

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 
Enter fullscreen mode Exit fullscreen mode

Also call above method in user after create callback so that whenever user is created send confirmation email:

after_create :send_confirm_email 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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" 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

Final code of all three parts is at:

GitHub logo 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!

Top comments (0)