JWT with Rails, Sorcery and AngularJS
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, includesRack::ConditionalGet
andRack::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:
- 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.
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);
});
// ...
}
}
How do we handle a situation when a user want to change his/her current password to new password using Rails + Devise + JWT? Please help me and share some resources. Thanks.