Programming

JWT with Rails, Sorcery and AngularJS

April 1, 2015
--
User avatar
Adrian Perez
@blackxored

JWT (short for JSON Web Token) is a compact URL-safe means of representing claims to be transferred between two parties, as defined by the standard. It's usually used for authentication and recently is being favored over the classic cookie scheme in Single Page Applications (SPAs).

Although cookies and server-side authentication are the most established solutions, for APIs usually better alternatives are OAuth2 and JWT.

This post assumes some level of familiarity, but should be easy to follow, visiting the homepage that I linked before should suffice for most of the code samples, if you want me to do a resources recommendation to dig deeper you can check Intridea's blog post, another one by Toptal (it's focused on Laravel, but the introductory section it's worth reading), or if you want to go all the way you check this PluralSight course on OAuth2, OpenID Connect and JWT.

Let's explore how to add JWT to a Rails and AngularJS codebase.

Implementing JWT on the server-side

We're gonna be looking at how to implement authentication and API resource access to a Ruby on Rails application.

Quick notes about the app and the API

Since it's common practice in the Rails community for documentation and tutorials we're going to lack imagination and say that our application is indeed a blog app.

It uses the Grape API framework, representable for model serialization (presenter), and a few features of a project of mine called Radriar. Also, while the code should be pretty self-contained, there are a few things worth noticing:

  • #represent and #represent_each on Grape endpoints: Radriar adds this method that among other things infers and uses a representer for the model or collection in question.
  • Representers are the core feature of representable, this post doesn't include them since they're pretty basic and don't really add anything to the topic.
  • Base API superclass: A base class for all versioned API endpoints, includes Rack::ConditionalGet and Rack::Etag middleware, basically because of the -removed from examples- caching with garner.

Adding dependencies

Let's get started by adding a few dependencies that we're going to need, each of them explained further throughout this post.

In Gemfile:

gem 'sorcery'
gem 'validates_email_format_for'
gem 'jwt'

Using Sorcery to add authentication

We will be using Sorcery to add authentication to our application, mainly because of being a simple and stripped-down auth library that doesn't make big assumptions for us.

I know a lot of users swear by Devise, but the main advantage to the library we've chosen is that we are completely in charge of defining the authentication flow, which makes perfect sense since we're creating one that would leverage JWT. Generally speaking, I've found Devise to be a bit overkill when you have an API-only server, and of course they're other reasons and scenarios you might want to use Sorcery instead of Devise, or the other way around, but they're outside the scope of this post.

We'll start by modifying our User model to wire it up with the authenticates_with_sorcery! line:

In app/models/user.rb:

class User
  include Mongoid::Document
  authenticates_with_sorcery!
      
  validates :username, presence: true, uniqueness: true
  validates :password, length: { minimum: 6 }, on: :create
  validates :email, uniqueness: true, email_format: true
end

That's all it takes to enable authentication behavior for this class. Also notice the use of validates_email_format_of for the email validation, this will ensure that the email format is valid (RFC-valid) and it's also customizable.

The avid reader might notice that we're missing any field declarations, that's because our model only uses the ones already provided by the library itself.

Enabling authentication in the API

Of course we couldn't stop there, because the models themselves need a way to be accessed from the outside world and controlled, hence we need to dive into our API layer (which might just be the controller layer if you're not using an API framework).

We're in need of the following pieces:

  1. A component that would create and validate tokens.
  2. A way to ensure we're authenticated and retrieve the current user in the API.
  3. A way of "login" users in.

Implementing the core JWT functionality

The TokenProvider is a generic service that is going to be responsible for validating and creating tokens that we'll use for users of our application, by using the jwt library that we added as a dependency. For the dynamic part of the token, we're going to keep it simple and use the Rails' application secret itself.

The service itself is decoupled, and we're going to introduce the implementation details at the API layer (such as the user_id, more on that later).

In app/services/token_provider.rb:

module TokenProvider
  class << self
    def issue_token(payload)
      JWT.encode(payload, Rails.application.secrets.secret_key_base)
    end
        
    def valid?(token)
      begin
        JWT.decode(token, Rails.application.secrets.secret_key_base)
      end
    end
  end
end

Adding an authentication helper to our API

At the bare minimum for any authentication solution, although semantics may vary, we need two methods to be available: #current_user and #authenticate.

We're going to create an authentication helper module that will provide functionality that will be included by our API definitions, to keep things DRY. It has before_action-style methods for authentication, token validation, etc.

While it might not be immediately obvious (don't worry, it will once we enter the client-side part of it), notice that the token is expected to be past via the Authorization request parameter in the form of Bearer <TOKEN>.

In app/api/blog/v1/auth.rb:

module Blog::V1::Auth
  def validate_token!
    begin
      TokenProvider.valid?(token)
    rescue
      error!('Unauthorized', 401)
    end
  end
  
  def authenticate!
    begin
      payload, header = TokenProvider.valid?(token)
      @current_user = User.find_by(id: payload['user_id'])
    rescue
      error!('Unauthorized', 401)
    end
  end
  
  def current_user
    @current_user ||= authenticate!
  end
  
  def token
    request.headers['Authorization'].split(' ').last
  end
end

In app/api/blog/api_v1.rb:

helpers Blog::V1::Auth

User registration and login

As part of our API, we're going to enable registration, hence we need a proper endpoint for creating new users. We're also going to need "login", which is actually an exchange flow from user credentials to a JWT, which might look familiar if you're familiar with OAuth2.

We also extend the user representer for this particular login request with a newly-generated token, here's where we issue a token.

In app/api/blog/v1/users.rb:

module Blog::V1
  class Users < Base
    helpers do
      def represent_user_with_token(user)
        represent(user).merge(token: ::TokenProvider.issue_token(
          user_id: user.id
        ))
      end
    end
    
    resource :users do
      params do
        requires :username
        requires :email
        requires :password
      end
      post do
        user = User.new(declared(params))
        user.save!
        represent_user_with_token(user)
      end
      
      params do
        requires :email
        requires :password
      end
      post "login" do
        user = User.find_by(email: params[:email])
        if user = User.authenticate(params[:email], params[:password])
          represent_user_with_token(user)
        else
          error!("Invalid email/password combination", 401)
        end
      end
    end
  end
end

Securing resources and using user-scoped data

Now that we have our authentication implemented at the model and controller level, we only need to use them in our specific endpoints.

Since they are the most common scenarios, in this -albeit contrived- example, posts could only be accessed if authenticated and the collection that is returned is that of only the posts belonging to the authenticated user.

Our endpoint definition would look like the following:

In app/api/blog/v1/posts.rb:

module Blog::V1
  class Posts < Base
    before do 
      authenticate!
    end
    
    desc "Get a list of my awesome posts"
    get do
      represent_each current_user.posts.ordered
    end
  end
end

And that would be all that's needed for a very simple implementation of JWT from top to bottom on the server.

Let's jump ahead to the consumer of this interface, or just "the client".

Implementing JWT on the client-side

In the Angular side of things, our walkthrough of the implementation would take an slighty different approach and order, we will start with the components from the bottom first.

Adding an authentication service

We need to define an authentication service to replicate the same authentication and current user functionality that we described as fundamental in the server section, in order to ensure that we can access authenticated resources, negotiate a token with the server, and respond properly to errors.

Adding a request/response interceptor

At the core of our implementation is an $http interceptor, this ensures we append the token to every request, and also redirect to login in case of unauthorized or unauthenticated messages.

In src/app/routes.js:

  $httpProvider.interceptors.push('AuthInterceptor'); 

In src/components/auth/auth.interceptor.js:

(function() {
  'use strict';

  function AuthInterceptor($q, $injector) {
    return {
      request: function(config) {
        var LocalService = $injector.get('LocalService');
        var token;

        if (LocalService.get('auth_token')) {
          token = LocalService.get('auth_token');
        }

        if (token) {
          config.headers.Authorization = 'Bearer ' + token;
        }

        return config;
      },
      responseError: function(response) {
        var LocalService = $injector.get('LocalService');
        // TODO: revisit for the 403
        if (response.status === 401 || response.status === 403) {
          LocalService.unset('auth_token');
          $injector.get('$state').go('login');
        }

        return $q.reject(response);
      }
    }
  }

  AuthInterceptor.$inject = ['$q', '$injector'];

  angular.module('blog.auth').factory('AuthInterceptor', AuthInterceptor);
})();

LocalService is just a wrapper to the localStorage browser API, and it should be pretty self-explanatory even without seeing its implementation.

Adding the Auth service

The next piece of the puzzle would be our actual service, which will be responsible of checking if the user is authenticated, login in, registering, etc.

Let's start with checking if the user is authenticated, given the way our interceptor was implemented, it's pretty easy:

In src/components/auth/auth.service.js:

function Auth($http, LocalStorageService, API_URL)) {
  return {
    isAuthenticated: function() {
      return LocalStorageService.get('auth_token');
    }
    // ...
}

Auth.$inject = ['$http', 'LocalService', 'API_URL', '$rootScope'];

angular.module("blog.auth").factory("Auth", Auth);

For the rest of this section, we won't repeat the file name or the code we've just shown, as it's the same.

Login and logout are pretty straightfoward themselves, if you remember from the server section, on login we would get a JSON response representing the User with a token:

{
  // ...
  login: function(credentials) {
    var login = $http.post(API_URL + '/login', credentials);
      login.success(function(result) {
        LocalStorageService.set('auth_token', result.token);
        var user = { 
          id: result.id, 
          username: result.username,
          avatarUrl: result.avatarUrl
        }
        LocalService.set('user', JSON.stringify(user)); 
      });
      
      return login;
  },
  logout: {
    LocalService.unset('auth_token');
    LocalService.unset('user');
  }

Technically, only the auth_token is strictly necessary, but you can see here how it can be used to also store the user object itself.

Finally, let's take a look at the registration functionality:

{
  // ...
  register: function(formData) {
    LocalService.unset('auth_token');
    var register = $http.post(API_URL + '/users', formData);
    register.success(function(result) {
      LocalService.set('auth_token', result.token);
    });
    
    return register;
  }
}

Implementing the controllers

In src/components/auth/registrations.controller.js:

(function() {
  'use strict';
  
  function Registrations(Auth, $state, $scope) {
    var vm = this;
    vm.errors = [];
  
    vm.register = function() {
      if ($scope.registerForm.$valid) {
        Auth.register(vm.user).then(function() {
          $state.go('posts.list');
        }, function(err) {
          vm.errors.push(err);
        });
      }
    };
  }
  // ...
  Registrations.$inject = ['Auth', '$state', '$scope'];

  angular.module('blog.auth').controller('Registrations', Registrations);
})();

The login controller is (almost alarmly) similar:

In src/components/auth/logins.controller.js:

function Logins($scope, $state, Auth) {
  // ...
  vm.login = function() {
    if ($scope.loginForm.$valid) {
      vm.errors = [];
      Auth.login(vm.user).success(function() {
        $state.go('posts.list');
      }).error(function(err) {
        vm.errors.push(err);
      });
     // ...
    }
  }
}

Conclusions

...

~ EOF ~

Craftmanship Journey
λ
Software Engineering Blog

Stay in Touch


© 2020 Adrian Perez