Ruby on Rails - JWT with Rails, Sorcery and AngularJS - ruby on rails tutorial - rails guides - rails tutorial - ruby rails



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

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'
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

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.
  • We 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, we'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:

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
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • 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 avoid 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:
    • A component that would create and validate tokens.
    • A way to ensure we're authenticated and retrieve the current user in the API.
    • A way of "login" users in.
ruby on rails tutorial tags - ruby , rail , ruby on rails , rail forum , ruby on rails tutorial , ruby tutorial , rails guides , rails tutorial , learn ruby

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

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
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

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

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
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

app/api/blog/api_v1.rb:

helpers Blog::V1::Auth
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

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.

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
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

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:

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
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • 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 a slightly 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.

src/app/routes.js:

$httpProvider.interceptors.push('AuthInterceptor'); 
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
ruby on rails tutorial tags - ruby , rail , ruby on rails , rail forum , ruby on rails tutorial , ruby tutorial , rails guides , rails tutorial , learn ruby

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);
})();
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • 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:

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);
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • 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 straightforward 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');
  }
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • 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;
  }
}
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

Implementing the Controllers:

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);
})();
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team
  • The login controller is (almost alarmly) similar:

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);
      });
     // ...
  }
}
Clicking "Copy Code" button will copy the code into the clipboard - memory. Please paste(Ctrl+V) it in your destination. The code will get pasted. Happy coding from Wikitechy - ruby on rails tutorial - rails guides - ruby rails - rubyonrails - learn ruby on rails - team

This ruby on rails tutorial page provides you the following key areas such as ruby , rail , ruby on rails , rail forum , ruby on rails tutorial , ruby tutorial , rails guides , rails tutorial , learn ruby , rails form_for , ruby rails , ruby class , what is ruby on rails , rails installer , ruby online , learn ruby on rails , ruby on rails jobs , rails find_by , install rails , easyrail , rubyonrails , link_to rails , ruby on rails developer , learn ruby the hard way , railscasts , ruby on rails examples , ruby on rails vs php , rails 4 , rails activerecord , rails generate , ruby and rails , ruby on rails download , install ruby on rails , ruby net http , what is rails , ruby app , ruby vs ruby on rails , ruby on rails windows , rails for zombies , ruby on rails book , ruby on rails development , ruby on rails ide , ruby on rails tutorial pdf

Related Searches to JWT with Rails, Sorcery and AngularJS