Implementing JWT In Auth Endpoints

Implementing JWT in our login and register endpoints

A Quick Recap

In the last section we created an AccessToken singleton class to help DRY up our code. In this section we will be utilizing that AccessToken class.

Feel free to refer to the part 10 branch of the GitHub repository if needed.

This is the 10th installment of my Developing A Cross-Platform iOS & Android Social Media App series.

Overview

The problem we are presented with is that we do not want to pass login information in every request. The only time a username and password should be passed in would be for the login and register endpoints. To solve this, we will pass in a JSON Web token in every request that isn't login/register. If no user is found, or the JWT is invalid/expired, the endpoint will return a 401.

Returning An AccessToken

We will first need to provide the user with an access_token when they login. Here is our current login method:

def login
  email = params[:user][:email]
  password = params[:user][:password]
  user = User.find_by(email: email)
  is_valid = user && user.valid_password?(password)
  unless is_valid
    render json: {
      status: 'error',
      message: 'Invalid credentials'
    }, status: 400 and return
  end

  return render json: user,
    meta: {access_token: '123'},
    status: 200
end

We still are hardcoding the access_token. Fix that by updating the render with:

payload = {user_id: user.id}
access_token = AccessToken.encode(payload)
return render json: user,
  meta: {access_token: access_token},
  status: 200

payload = {user_id: user.id}: creates a variable named payload that contains the user's id from the user object declared above

access_token = AccessToken.encode(payload): We use our AccessToken singleton class to encode our payload and assign it to access_token.

meta: {access_token: access_token},: replaces our hardcoded access_token with the correct access token.

Now, when we hit the login endpoint with curl, we get the access_token (save it for later):

$ curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   307    0   256  100    51   1363    271 --:--:-- --:--:-- --:--:--  1368
{
   "user" : {
      "id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
      "email" : "[email protected]"
   },
   "meta" : {
      "access_token" : "eyz1...5dX"
   }
}

Setting Up The Application Controller

Don't forget that our UserController inherits ApplicationController:

class V1::UsersController < ApplicationController
  ...
end

This means that we can handle the access_token logic in our ApplicationController by validating the Authorization header.

An Authorization header can be passed in to our request so that APIs will know who is making the request. For JWT, these are usually in the format of Bearer JSON_WEB_TOKEN. We can append an Authorization header to our request by adding -H "Authorization: Bearer eyz1...5dX" to our curl command.

Rails can now access this by using request.headers['Authorization']. Lets take a look at how we can verify the auth header. Create the following private method in your app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  private
  def authenticate_request
    auth_header = request.headers['Authorization']
    regex = /^Bearer /
    auth_header = auth_header.gsub(regex, '') if auth_header
    @current_user = AccessToken.get_user_from_token(auth_header)
    render json: { error: 'Not Authorized' }, status: 401 unless @current_user
  end
end

private: Declares a private method.

def authenticate_request: The name of our method, we will call this method soon.

auth_header = request.headers['Authorization']: Pulls the Authorization header from the headers (request.headers) and assigns it to auth_header.

regex = /^Bearer /: Remember that the Authorization header is in this format: Bearer AUTH_TOKEN. We only need the token itself, so we will remove the Bearer from the string using regex. In Ruby, we can declare regex between /.../. The caret (^) signifies that it will match the string if it starts with whatever comes after the ^. In this case ^Bearer will match anything that starts with Bearer. For example /^abc/ will match abcd but not acd or aabc. (Do not forget the space in /^Bearer /!)

auth_header = auth_header.gsub(regex, ''): gsub allows us to substitute all occurrences of a pattern provided (our regex variable) and substitutes it with a second string ('' is equivalent to removing the occurrences). We then assign the new string to auth_header. This essentially removes Bearer from our token.

if auth_header: Adding an if or unless statement to the end of a line is a shorthand for wrapping that line with an if or unless block. So ...auth_header.gsub(...) will only run if auth_header is not nil or ''.

@current_user =: This creates an instance variable which means that we can use this variable in any method within the class. For example, if we want to return all of the user's posts, we can just write @current_user.posts in an endpoint.

AccessToken.get_user_from_token(auth_header): This called our singleton class to return the user from the token passed in.

render json: { error: 'Not Authorized' }, status: 401: This is the error message if the authorization fails. We will use a 401 HTTP status.

unless @current_user: An error response will be returned unless @current_user exists.

So when does this method run? Rails provides us with a before_action option that lets us specify what actions should be ran before each API call. In this case we want to use our authenticate_request method:

class ApplicationController < ActionController::API
  before_action :authenticate_request

  private
  def authenticate_request
    ...
  end
end

We are almost done. The issue we have is that the API will return Not Authorized for our login and register endpoints since it requires an access token. There is no way for a user to have their access_token without those endpoints. Rails provides us with a skip_before_action option for controllers. Add it for the login and register endpoints in user_controller.rb:

class V1::UsersController < ApplicationController
    skip_before_action :authenticate_request, only: [:login, :register]
  ...
end

skip_before_action: This takes in the actions as a parameter and does not run those before our endpoints.

:authenticate_request: The method we want to skip

only: [:login, :register]: The only parameter lets us set which specific endpoints that we do not want to implement JWT auth.

Testing Our JWT Auth

Create a test endpoint in the user_controller:

class V1::UsersController < ApplicationController
  skip_before_action :authenticate_request, only: [:login, :register]
  ...
  def test
    render json: @current_user, status: 200
  end
  ...
end

This endpoint is very simple, it just returns the @current_user instance variable from our JWT auth method. Update the routes.rb:

Rails.application.routes.draw do
  concern :base_api do
    post 'users/register', to: 'users#register'
    post 'users/login', to: 'users#login'
    get 'users/test', to: 'users#test'
  end

  namespace :v1 do
    concerns :base_api
  end
end

Restart your server using ctrl+c and $ rails c. We can hit the /v1/test endpoint using curl. If you do not have your access_token, hit the login endpoint again and copy it.

curl -H "Content-Type: application/json" -X POST -d '{"user":{"email": "[email protected]","password":"123456"}}' http://localhost:3000/v1/users/login | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   307    0   256  100    51   1041    207 --:--:-- --:--:-- --:--:--  1044
{
   "meta" : {
      "access_token" : "eY23...fD2"
   },
   "user" : {
      "id" : "7545d290-962f-45bb-891d-f7e43b4fbf68",
      "email" : "[email protected]"
   }
}

Pass the access_token as an Authorization header via -H "Authorization: Bearer eY23...fD2", change POST to GET, and change the endpoint to test:

curl -H "Content-Type: application/json" -H "Authorization: Bearer eY23...fD2" -X GET http://localhost:3000/v1/users/test | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   130    0    79  100    51   5923   3824 --:--:-- --:--:-- --:--:--  6076
{
   "user" : {
      "email" : "[email protected]",
      "id" : "0e6e9b1c-22f5-41a7-91bb-3da9808c7b3e"
   }
}

If everything works, you're finally done! At this point we should have a very solid foundation for a Rails API. This is a good stopping point for us to introduce tests, which we will cover in the next section.

Part 11 (Creating Rails Tests) will be released soon. Please check back later.