It's good to be awesome
Nikola Todorovic
  • Home
  • Hire Me
  • Nikola's Blog
  • Professional Career
  • Biography
  • Photo Gallery
  • Contact

RAILS 4 + ANGULARJS + JSON WEB TOKEN AUTHENTICATION

5/8/2015

2 Comments

 
Recently I was contacted by Toptal to join their network as a developer. I heard some good stuff about them before so I decided to give it a go. Toptal is "a network comprised of the most thoroughly screened, talented freelance engineers in the world". To enter the network you have to pass 4 tests. The first one is an interview - a simple English test, the second one is Codility test. The third one is a one-to-one test with a senior developer from the network when you need to solve two problems at limited time. And the last one is to create a full application.
Anyway, I am not going to talk about Toptal, rather than the project they gave me to do as the last part of the screening process. The full text of the project as well as the solution you can find on my GitHub. In this post I will only go through the main things and make some general remarks how development should be done and what you should pay attention, I won't cover every detail because there is too much code. For all the details you have code on GitHub.

Authentication

Since I needed to create API with every operation done using JavaScript I decided to use Angular to create a SPA. And since there is an API and SPA I decided to authenticate users using tokens. I didn't want to use Devise since the basic gem doesn't offer token authentication and then you need to add some other gem to be able to do that so I decided to use so called JWT (JSON web token). More about that technology you can find here.
require 'jwt'

module AuthToken
  def AuthToken.issue_token(payload)
    payload[:exp] = Time.now.to_i + 4 * 3600
    JWT.encode payload, Rails.application.secrets.secret_key_base
  end

  def AuthToken.valid?(token)
    begin
      JWT.decode token, Rails.application.secrets.secret_key_base
    rescue
      false
    end
  end
end
Token generation, encoding and decoding is very simple using existing ruby gem. Only thing you need to do is gather everything in a whole. The general idea when user needs to be authenticated is to generate an API call with the parameters of email and password. If it turns out that this is the valid user, the token is being generated and returned to the user as a JSON response. That response also contains a data what user it is (user_id). The whole token is saved in the browser's local storage. Instead, a token can be stored in a cookie (therefore cookie is used as a storage), more about that you can find on this link.
class AuthController < ApplicationController
  require 'auth_token'

  layout false

  def register
    @user = User.new(user_params)
    if @user.save
      @token = AuthToken.issue_token({ user_id: @user.id })
    else
      render json: { errors: @user.errors }, status: :unauthorized
    end
  end

  def authenticate
    @user = User.find_by(email: params[:email].downcase)
    if @user && @user.authenticate(params[:password])
      @token = AuthToken.issue_token({ user_id: @user.id })
    else
      render json: { error: "Invalid email/password combination" }, status: :unauthorized
    end
  end

  def token_status
    token = params[:token]
    if AuthToken.valid? token
      head 200
    else
      head 401
    end
  end

  private

    def user_params
      params.permit(:first_name, :last_name, :email, :password)
    end
    
end
Then, each time the client sends a request to the API, the token is being added to the header of that request. To do this, an interceptor is created on Angular side which intercepts each request and does this part of the job.
@topTalApp.factory('AuthInterceptor', ($location, $rootScope, $q, $injector) ->
  
  authInterceptor = {

    request: (config) ->
      token = undefined
      if localStorage.getItem('auth_token')
        token = angular.fromJson(localStorage.getItem('auth_token')).token
      if token
        config.headers.Authorization = 'Bearer ' + token
      config

    responseError: (response) ->
      if response.status == 401
        localStorage.removeItem 'auth_token'
        $rootScope.errorMsg = response.data.error
        $location.path '/login'
      if response.status == 403
        $rootScope.errorMsg = response.data.error
        $location.path '/'
      $q.reject response

  }
  
  authInterceptor

).config ($httpProvider) ->
  $httpProvider.interceptors.push 'AuthInterceptor'
  return
On the API side, the token is being extracted from the header of that request and checked whether it is valid (the expiration time is being set on the token). If it is valid than user_id is being used to get data for current_user. If token is invalid or there isn't any token, API returns http code 401 - which prevents unauthorized access to the API if someone is not authenticated...
module Api
  class BaseController < ApplicationController
    require 'auth_token'

    before_action :authenticate

    # jbuilder needs this
    layout false

    private

      def authenticate
        begin
          token = request.headers['Authorization'].split(' ').last
          payload, header = AuthToken.valid?(token)
          @current_user = User.find_by(id: payload['user_id'])
        rescue
          render json: { error: 'You need to login or signup first' }, status: :unauthorized
        end
      end

  end
end
And let's not forget about routes. This is how I defined routes. This is pretty straightforward but if you need some extra explanation about it you can find an excellent RailsCast episode about APIs.
Rails.application.routes.draw do
  root 'home#index'

  namespace :api, defaults: {format: :json} do
    resources :users, except: [:new, :edit]
    resources :expenses, except: [:new, :edit] do
      collection do 
        get :weekly
      end
    end
  end

  post '/auth/register', to: 'auth#register', defaults: {format: :json}
  post '/auth/authenticate', to: 'auth#authenticate', defaults: {format: :json}
  get '/auth/token_status', to: 'auth#token_status', defaults: {format: :json}

end

Authorization

Authorization is certainly another important thing in every application but unfortunately it is something that is not paying too much attention. Of course, everyone has their own idea of how authorization should be implemented and I don't think there is any general best practice but I think that this approach, which I applied here, is more than good. It is necessary to do authorization on the API side and also in the web application. On API side because calls can be directed only to an API without any application involved and within application users without enough permissions should be forbidden access to the certain parts of the application as well as the forms for inserting new data.
I defined three types of user in the application - regular user, admin and user manager. A regular user has permission to only list her expenses and to create them, user manager has the right to list all the users in the application, edit them and create a new one, admin has the right to create expenses for herself and to list and edit expenses of all users in the system as well as to create new and edit existing users.
class User < ActiveRecord::Base

  has_secure_password

  validates :email, :password_digest, presence: true
  validates :email, uniqueness: { case_sensitive: false }
  
  validates :password, length: { minimum: 5 }

  has_many :expenses
  
  belongs_to :role

  ROLES = {
    REGULAR: 1,
    ADMIN: 2,
    MANAGER: 3
  }

  def is_regular
    self.id_role == ROLES[:REGULAR]
  end

  def is_admin
    self.id_role == ROLES[:ADMIN]
  end
  
  def is_manager
    self.id_role == ROLES[:MANAGER]
  end

end

- Api

I didn't want to use Cancancan or Pundit or similar gems because I wanted to save myself some time and the authorization on the API is more than simple. All you have to do is to create a before_action filter and each controller then has to define who has the right to access the actions within it. If someone doesn't have permissions then you need to return http code :forbidden. This of course can't be tested in the application but there are tests you can write to check if everything is fine.
module Api
  class UsersController < Api::BaseController
    before_action :is_authorized?

    private

      def is_authorized?
        if @current_user.is_regular
          render json: { error: "Doesn't have permissions" }, status: :forbidden
          return
        end
      end

  end
end

- Angular

Angular part is a little more complex... First you should remove links from menu if certain user doesn't have permissions and then you should also disable the possibility for the same users to manually enter routes in the browser, which would allow them to visit those pages. It is pretty much easy to disable links in the menu but to do the other part you need to put some more effort in it. The most important part can be found in the file angular/services/auth.coffee:
@topTalApp.factory 'Auth', ['$http', 'CurrentUser', 'ROLES', ($http, CurrentUser, ROLES) ->

  currentUser = CurrentUser

  token = localStorage.getItem('auth_token')

  {
    isRegularUser: (user) ->
      user.getRole() == ROLES.REGULAR

    isAdminUser: (user) ->
      user.getRole() == ROLES.ADMIN

    isManagerUser: (user) ->
      user.getRole() == ROLES.MANAGER

    isAuthorized: (permissions) ->
      i = 0
      while i < permissions.length
        switch permissions[i]
          when 'REGULAR'
            return true if currentUser.getRole() == ROLES.REGULAR
          when 'ADMIN'
            return true if currentUser.getRole() == ROLES.ADMIN
          when 'MANAGER'
            return true if currentUser.getRole() == ROLES.MANAGER
        i += 1
      return false
  }

]
Then, you should listen to a $routeChangeStart event and disable access to a page if user doesn't have needed permissions. The code is in the file angular/app.coffee:
@topTalApp = angular.module('topTalApp', ['ngRoute', 'rails', 'templates', 'ui.bootstrap', 'sy.bootstrap.timepicker', 'angularUtils.directives.dirPagination'])

@topTalApp

  .run [
    '$rootScope'
    '$location'
    'Auth'
    ($rootScope, $location, Auth) ->
      $rootScope.$on '$routeChangeStart', (event, next, current) ->
        if next.access != undefined and !Auth.isAuthorized(next.access.requiredPermissionsAnyOf)
          if next.templateUrl == 'expenses/expenses.html' and Auth.isAuthenticated() != null
            $location.path '/users'
          else if next.templateUrl == 'expenses/expenses.html' and Auth.isAuthenticated() == null
            $location.path '/login'
          else
            $location.path '/'
        return
      return
  ]
  
  .config(
    ($routeProvider) ->
    
      $routeProvider
        .when '/signup', {templateUrl: 'sessions/signup.html', controller: 'SignupCtrl'}
        .when '/login', {templateUrl: 'sessions/login.html', controller: 'LoginCtrl'}

        .when '/', {
          templateUrl: 'expenses/expenses.html',
          controller: 'ExpenseCtrl',
          access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ]
        }

        .when '/expenses', {
          templateUrl: 'expenses/expenses.html',
          controller: 'ExpenseCtrl',
          access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ]
        }

        .when '/new_expense', {
          templateUrl: 'expenses/new_edit_expense.html',
          controller: 'ExpenseCtrl',
          access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ]
        }

        .when '/expense/:id', {
          templateUrl: 'expenses/new_edit_expense.html',
          controller: 'ExpenseCtrl',
          access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ]
        }

        .when '/expenses/weekly', {
          templateUrl: 'expenses/weekly.html',
          controller: 'ExpenseWeeklyCtrl',
          access: requiredPermissionsAnyOf: [ 'REGULAR', 'ADMIN' ]
        }

        .when '/users', {
          templateUrl: 'users/users.html',
          controller: 'UserCtrl',
          access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ]
        }

        .when '/new_user', {
          templateUrl: 'users/new_edit_user.html',
          controller: 'UserCtrl',
          access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ]
        }

        .when '/user/:id', {
          templateUrl: 'users/new_edit_user.html',
          controller: 'UserCtrl',
          access: requiredPermissionsAnyOf: [ 'MANAGER', 'ADMIN' ]
        }

        .otherwise({redirectTo: '/'})

  )
I think this is more than elegant solution for authorization. The whole code you can find on GitHub as I mentioned before.

Rails API

When you set up the basics of the application with authentication and authorization then you must set up the API part of the Rails. There is an excellent RailsCast about it and many tutorials online so I won't go into details. Of course, you can use Grape instead of Rails but I decided to stick with Rails because I've never before worked with Grape and I wanted to save time on this part too. The thing you have to keep in mind are the routes and the fact that each API call must return JSON response. You can use active model serializer or Jbulder or you can even go without that of course. But I used Jbulder because you definitely need an easy way to properly create JSON you want to return to the user that made the request. Again I have to mention that you have a great RailsCast episode how to use Jbuilder so I won't write on this topic further.

Angular app

This was a new ground for me, I've never built a SPA before but for two weeks how long did this project last I am very satisfied with what I've achieved. There are several ways to integrate your Rails API with Angular application. Again I chose the simplest option, although probably not the best. I used Angular gem. I think that in a bigger project the best way is to completely separate API part of its SPA part and not use this gem but in this situation it served the purpose really well.
I won't post pieces of the code here because you have a whole project on GitHub, but I would like to note that I used Slim for making templates, CoffeeScript instead of a plane JavaScript because I like cleaner code and I used Bootstrap too. I made a couple of filters in order to extract information about date and time from the column in the database that stores datetime together for nicer display. Everything else is pretty straightforward.

Timezone

The part of the project that I had the most headache is the part with printing data for expenses arranged by weeks. Why was this such a big problem? As I wrote in a previous blog post, it's very important how you work with time zones in your application. The best practice is to store everything in UTC time in the database. When a user enters a date and time in the form (her local time) you have to store that time in the UTC format in the database and then when you need to display that time to the user you have to convert it back into her local time. Pretty standard stuff... Except for one little thing - the requirement is to display expenses arranged by weeks and by weeks it's meant from the user's perspective. From user's perspective a week isn't the same as a week on the server which is in UTC time. So, you have to deal with UTC time because you have to prepare JSON response in your API but in the same time you have to deal with user's local time (starting and ending of the week) and arrange JSON response according to that. If a user is, for example, in Belgrade (CET) and if she inserts an expense for Monday, July 27th at 1 AM it would be 31st week of the year according to her. But by the server's time, it is Sunday July 26th at 11 PM, which means it is 30th week for the UTC time. I solved this problem by sending time zone offset as a parameter to the API and by adding it to a time I stored in the database. You must also pay attention to the edge cases when you calculate weeks when start of a week is in one year and its ending is in the next year.
def weekly
  expenses = findByDateFromAndTo params[:datefrom], params[:dateto], :asc
  my_json = {}

  add_minutes =  - params[:timezone].to_i.minutes

  expenses.each do |expense|
    year, month, week = (expense.for_timeday + add_minutes).strftime('%Y'), (expense.for_timeday + add_minutes).strftime('%m'), (expense.for_timeday + add_minutes).strftime('%V')
    year = (year.to_i + 1).to_s if month.to_i == 12 && week.to_i == 1
    key = year + '.' + week

    analytics = my_json[key] || {}
    analytics[:expense] = analytics[:expense] || []
    analytics[:total] = expense.amount + (analytics[:total] || 0)
    analytics[:items] = 1 + (analytics[:items] || 0)
    analytics[:start] = Date.commercial(year.to_i, week.to_i, 1).to_s
    analytics[:end] = Date.commercial(year.to_i, week.to_i, 7).to_s

    my_expense = {for_timeday: expense.for_timeday, amount: expense.amount, description: expense.description, comment: expense.comment}
    analytics[:expense].push my_expense

    my_json[key] = analytics
  end

  render json: my_json.values
end

TestS

Last but not least - tests! Writing tests is very important and you shouldn't neglect it even if you have tight deadline. I did lot more here than it was expected from me but unfortunately I didn't have time to write tests for JavaScript, but the procedure is pretty standard... I used Factory Girl and Faker to create data and also Database Cleaner to clean data after running tests.
FactoryGirl.define do

  factory :expense do
    
    association :user, factory: :user_regular

    amount { Faker::Commerce.price }
    for_timeday { Faker::Time.between(100.days.ago, 1.day.ago) }
    description { Faker::Lorem.sentence }
    comment Faker::Lorem.sentence

    trait :future_time do
      for_timeday { Faker::Time.forward(5, :all) }
    end

    factory :expense_future, traits: [:future_time]

  end

end
Next, you should write unit tests - tests for models, since this is not a big database there is only two models to test, user and expense:
require 'rails_helper'

describe Expense do

  it 'has a valid factory' do
    expect(build(:expense)).to be_valid
  end

  it 'is invalid without an amount' do
    expect(build(:expense, amount: nil)).to_not be_valid
  end

  it 'is invalid without a description' do
    expect(build(:expense, description: nil)).to_not be_valid
  end

  it 'is invalid without a time' do
    expect(build(:expense, for_timeday: nil)).to_not be_valid
  end

  it 'is invalid with a time set in the future' do
    expect(build(:expense_future)).to_not be_valid
  end

  it 'should belong to a user' do
    expense = build(:expense)
    user = build(:user)
    expense.user = user
    expect(expense.user).to be user
  end

end
And finally functional tests - for testing your controllers. I am especially proud of this part of the application since I covered entire API - authentication, authorization as well as creating expenses, users and so on...
require 'rails_helper'

describe AuthController do

  # because of the jBuilder I need to render views
  render_views

  describe 'POST #register' do

    context 'with valid credentials' do

      it 'returns user id' do
        #build a user but does not save it into the database
        user = build(:user_regular)
        post :register, { email: user.email, password: user.password, format: :json }
        expect(response.status).to eq 200
        parsed_response = JSON.parse response.body
        expect(parsed_response['user']['id']).to_not be_nil
      end

    end

    context 'with invalid credentials' do

      it 'does not have email' do
        post :register, { password: "pass", format: :json }
        expect(response.status).to eq 401
        parsed_response = JSON.parse response.body
        expect(parsed_response['errors']).to_not be_nil
        expect(parsed_response['errors']['email'][0]).to eq "can't be blank"
      end

      it 'does not have password' do
        post :register, { email: "email@email.com", format: :json }
        expect(response.status).to eq 401
        parsed_response = JSON.parse response.body
        expect(parsed_response['errors']).to_not be_nil
        expect(parsed_response['errors']['password'][0]).to eq "can't be blank"
      end

    end

  end


  describe 'POST #authenticate' do

    context 'with valid credentials' do

      it 'returns token' do
        user = create(:user_regular)
        post :authenticate, { email: user.email, password: user.password, format: :json }
        expect(response.status).to eq 200
        parsed_response = JSON.parse response.body
        expect(parsed_response['token']).to_not be_nil
      end

      it 'returns token with 3 parts separated by comas' do
        user = create(:user_regular)
        post :authenticate, { email: user.email, password: user.password, format: :json }
        expect(response.status).to eq 200
        parsed_response = JSON.parse response.body
        expect(parsed_response['token'].split('.').count).to eq 3
      end

      it 'returns first name and last name of the user' do
        user = create(:user_regular)
        post :authenticate, { email: user.email, password: user.password, format: :json }
        expect(response.status).to eq 200
        parsed_response = JSON.parse response.body
        expect(parsed_response['user']['first_name']).to eq user.first_name
        expect(parsed_response['user']['last_name']).to eq user.last_name
      end

    end

    context 'with invalid credentials' do

      it 'does not return token' do
        user = create(:user_regular)
        post :authenticate, { email: "no_" + user.email, password: user.password, format: :json }
        expect(response.status).to eq 401
      end

    end

  end

  describe 'POST #token_status' do

    context 'with valid token' do

      it 'returns OK code' do
        user = create(:user_regular)
        token = AuthToken.issue_token({ user_id: user.id })
        post :token_status, { token: token, format: :json }
        expect(response.status).to eq 200
      end

    end

  end

end
Again, the rest of the code you can find on GitHub :)

The end

If you are a great developer and you want to try yourself to do some similar project, and if you want to work for the Toptal as a freelance developer, to fulfill your dreams to work from home or some exotic island you can sign up here, I enjoyed participating in such a process of selection of candidates.
2 Comments
martin link
6/5/2016 13:43:47

ybavo

Reply
Avondale Tree Removal link
11/8/2022 20:56:00

Thank yyou for sharing

Reply



Leave a Reply.

    Nikola Todorovic

    Software developer from Belgrade, founder of Warrantly.

    Archives

    December 2015
    August 2015
    March 2015
    January 2015
    December 2014
    October 2014
    September 2014
    August 2014
    June 2013
    December 2012
    November 2012

    Categories

    All
    Ajax
    Angular
    API
    DataTables
    Hibernate
    Holiday
    Maven
    Metaprogramming
    Mysql
    Oracle
    Postgres
    Rails
    Ruby
    Sea
    Software
    Spring
    Startup
    Tomcat
    Turkey
    Website

    RSS Feed

Powered by Create your own unique website with customizable templates.