Wednesday, February 3, 2021

Authentication Rails

We'll only need two gems for handling all of our backend authentication logic.

Knock is similar to Devise in that it will help us to authenticate users as well as manage a user's session with a JSON Web Token (JWT). We can also prevent unauthorized users from doing things they're not supposed to.

Knock uses Bcrypt under the hood. It will encrypt the user's password to ensure that we don't store plain text passwords in our database.

  1. Add this to your Gemfile and uncomment bcrypt, run a bundle install

    gem 'knock', git: 'https://github.com/nsarno/knock', branch: 'master', ref: '9214cd027422df8dc31eb67c60032fbbf8fc100b'
    
  2. What we're missing right now is a User model, let's set that up, password_digest is the special field the user model needs for an encrypted password

    rails g model User email:string password_digest:string
    rails db:migrate
    
  3. Add this to our User model, the has_secure_password allows us to set and authenticate against the bcrypt password, we're also setting up some basic model validations

    class User < ApplicationRecord
      has_secure_password
      validates :email, presence: true
    end
    
  4. Add this to our application_controller, the include keyword in controller allows us to use the Knock methods in all of our controllers (like authenticate_user)

    class ApplicationController < ActionController::API
      include Knock::Authenticable
    end
    
  5. Generate a new controller named user_token_controller

    rails g controller user_token
    
  6. Make sure your user_token_controller inherits from the auth_token_controller which is setup by knock

    class UserTokenController < Knock::AuthTokenController
    end
    
  7. Create a a new file config/initializers/knock.rb, it contains all the config we need, as we're using the secret_key_base you need to ensure you have a credentials file and a master.key, when you deploy you'll also need to ensure the RAILS_MASTER_KEY is included as an ENV variable

    Knock.setup do |config|
      config.token_signature_algorithm = 'HS256'
      config.token_secret_signature_key = -> { Rails.application.credentials.secret_key_base }
      config.token_public_key = nil
      config.token_audience = nil
      config.token_lifetime = 1.day
      config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound'
    end
    
  8. Add this to your routes, the create action is setup by knock and we get it through the inheritance from the auth_token_controller

    post "/login", to: "user_token#create"
    
  9. To hit this endpoint we MUST already have a created user, run this command in rails c

    User.create(email: "[email protected]", password: "password")
    
  10. Start your server

  11. Send a POST request to http://localhost:3000/login, add the following JSON to the body of the request

    {
      "auth": {
        "email": "[email protected]",
        "password": "password"
      }
    }
    
  12. You should a response body that looks like this

    {
      "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTM0OTYzMjUsInN1YiI6MX0.ssEkAbN_ei_XzSOb7ClWwU6Ux1Zftas3GwEkow3tY-c"
    }
    
  13. You should test to see what happens if you pass a wrong email or password combination

  14. Generate a new controller named status_controller:

    rails g controller status
    
  15. Put this in the status controller, before_action :authenticate_user is the same as the Devise before_action :authenticate_user!

    class StatusController < ApplicationController
      before_action :authenticate_user
    
      def index
        render status: :ok
      end
    end
    
  16. Add this to your routes:

    get "/status", to: "status#index"
    
  17. Hit the /login endpoint again in postman and copy the JWT

  18. Inside postman setup a GET request to http://localhost:3000/status, and add the following to your headers:

    Authorization: Bearer <jwt you just copied>
    
  19. Run the request and you should get something like this back:

    200 ok
    
  20. Mess with the Authorization header and see if you can get a 401 status code, refresh your memory on what a 401 means

  21. Hit the /status endpoint again, we should see something like this:

    {
      "message": "logged in",
      "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTM0OTc1MTgsInN1YiI6MX0.QxW4bYH8XwekTXQgFWkNCwCsKXSpz1PaLlgxMNyJ2Dw"
    }
    
  22. We still need an endpoint for signing up as a new user, add a users_controller

    rails g controller users
    
  23. Add this to the controller:

    class UsersController < ApplicationController
      def create
        user = User.new(user_params)
        if user.save 
          render status: :created
        else
          render status: :bad_request
        end
      end
    
      private 
    
      def user_params 
        params.require(:user).permit(:email, :password)
      end 
    end
    
  24. Add this to routes:

    post "/sign-up", to: "users#create"
    
  25. Send a POST request to http://localhost:3000/sign-up, add the following JSON to the body of the request

    {
      "user": {
        "email": "[email protected]",
        "password": "password"
      }
    }
    
  26. You should a response body that looks like this

    201 created
    
  27. We now have endpoints for logging users in and signing users up, we also have endpoints that are protected unless the user has logged in

If you're keen on seeing the client side implementation check out this post.