JWT Token-based custom user authentication for Rails API only (Part 03)
Tags: rails, jwt, api, authentication
Published: 2021-05-28 08:24:57

The code available at:
{% github sulmanweb/rails-api-user-custom-auth no-readme %}

> 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:

```ruby
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](https://rubygems.org/gems/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:

```ruby
post "sign_in", to: "sessions#create"
get "validate_token", to: "sessions#validate_token"
delete "sign_out", to: "sessions#destroy"
```

![Sign In Service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9xwytojo8oura25uj35t.png)

![Validate Token Service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/glbxr4ztesllvzerf67j.png)

![Sign Out Service](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfjsrl7ml8c74b01r8hj.png)

## 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.

```ruby
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:

```ruby
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](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:

```ruby
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:

```ruby
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`:

```ruby
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`:

```ruby
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:

```ruby
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:

```bash
                            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:
{% github sulmanweb/rails-api-user-custom-auth no-readme %}

## 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!