Ruby on Rails Authentication: A JSON Web Token Example
So as part of the fair job offer project, a tool to help programmers keep track of their total compensation and make apples-to-apples comparisons of their job offers, I decided to experiment with using JSON web tokens for authentication.
What is JWT?
Briefly, JWT (JSON Web Token) is a standard that “is a compact, URL-safe means of representing claims to be transferred between two parties.”
JWT and Oauth2 are two popular ways of adding server side authentication for APIs. I’ll spare you the details of JWT since there are other sources for it. Just think of JWT data represented as an encoded JSON object.
Application Overview
This blog post is intended as a brief tour of the setup required for making JWT token authentication work with a Rails-based Grape API with Angular on the front end. Full source code is located at the fair_offer_calculator repository.
Authentication Data flow
Here is how the authentication scheme with JWT works in this application.
- A client username and password gets sent to the backend Ruby API.
- If the client credentials are valid, a JWT token is generated.
- The client receives the token and stores it somewhere for later use.
- If the client makes a request for a protected resource from the server, it includes the token in an HTTP header that includes the user’s id.
- When receiving a request, the backend server looks at the JWT token and verifies it was generated for the user represented by the ID in the HTTP payload.
Server side code
A lot of this code is based on a couple of blog posts I found from Adam A and Adrian P.
I’m using the clearance gem by Thoughtbot and an AuthorizationToken module built around the jwt gem for Ruby.
Step 1 – Build an AuthorizationToken Library
The first thing to do is to have an authorization token library that generates an encoded JWT token and checks if one sent back from the client is a valid token.
class AuthorizationToken
# Encode a hash in a json web token
def self.encode(payload, ttl_in_min = 60 * 24 * 10)
payload[:exp] = ttl_in_min.minutes.from_now.to_i
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end
# Decode a token and return the payload inside
# throw an error if expired or invalid. See the docs for the JWT gem.
# Note that the decode method could potentially raise on of several exceptions, so any calling code should account for this.
# source: http://adamalbrecht.com/2015/07/20/authentication-using-json-web-tokens-using-rails-and-react/
def self.valid?(token, leeway = nil)
begin
decoded = JWT.decode(token, Rails.application.secrets.secret_key_base, leeway: leeway)
HashWithIndifferentAccess.new(decoded[0])
end
end
end
Step 2 – API Signup and Login Endpoint
In the below example, you can see, I use the AuthorizationToken module I built in Step 1 for both the signup and login endpoints.
require 'authorization_token'
module JobOfferUser
class V1 < Grape::API
version 'v1'
format :json
helpers Auth
helpers do
def represent_user_with_token(user)
present user, with: Entities::JobOfferUser, jwt_token: AuthorizationToken.encode({user_id: user.id})
end
end
resource :users do
params do
optional :first_name
optional :last_name
requires :email
requires :password
end
post "/signup" do
user = User.new(declared(params))
if user = user.tap(&:save)
represent_user_with_token(user)
else
error!("Invalid email/password combination", 401)
end
end
params do
requires :email
requires :password
end
post "/login" do
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
Front end code
The front end Angular controller code is a bit more complicated than what is shown below. I’m simply showing the main functionality needed to talk to the signup and login endpoints of the API.
Step 3 - Login controller
In both the login and signup Angular controllers, I use the Restangular library to make HTTP requests to send and receive the JWT token.
function signupCtrl(
$scope,
$location,
localStorageService,
Restangular,
AuthFactory,
) {
$scope.signupUser = function() {
var signup = Restangular.all('api/v1/users/signup');
signup.post($scope.user).then(
function(response) {
AuthFactory.authenticate(response); //response is basically user object
$location.url('/fair_offer');
},
function() {
console.log('There was an error in getting jwt token.');
},
);
};
}
function loginCtrl(
$scope,
$location,
localStorageService,
Restangular,
AuthFactory,
) {
$scope.loginUser = function() {
var login = Restangular.all('api/v1/users/login');
login.post($scope.user).then(
function(response) {
AuthFactory.authenticate(response); //response is basically user object
$location.url('/fair_offer');
},
function() {
console.log('There was an error in getting jwt token.');
},
);
};
}
Summary
With Rails and Angular, the basic setup for getting JWT authentication working isn’t too bad.
- Full source code - fair_offer_calculator repository.