JWT Fundamentals - How To Easily Share UI Components

Posted May 11, 2017 by Tim Upchurch in Development

When developing a web application, it is often appropriate to divide responsibilities across different "services" that each handle discrete functionality. While this can provide flexibility in the behavior of an application, consistency across branded UI components becomes difficult, as each service needs to maintain its own set of component markup, styles, and scripts.

Background

As a web development consultancy, we are often presented with the case where a client wants to have control over the marketing content that frames their application. This request introduces an opportunity to split the application's responsibilities, breaking away from our base stack- Ruby on Rails- and bringing in something more tailored to content management- like WordPress. Copy edits can be deferred to the client, who now has a GUI backend appropriate for working with content.

Splitting the application introduces more out-of-the-box functionality, but as mentioned, we've now created potential problems with maintaining consistency across front-end components. To discuss our method of addressing this problem, we'll look at this scenario in isolation.

Imagine that a large application is comprised of two separate parts- our distinct services. The first, named "rhino", is a database driven Ruby on Rails application. The second component, referred to as "hippo", is a landing page, simply using JavaScript to provide functionality- this will stand in for WordPress, as we'll only need to work with client-side functionality.

The Problem

In this fictitious scenario, hippo and rhino are designed to do what they do well; rhino, to handle the data model and business logic therein, and hippo, to be slick, fast, and JavaScript-y. Perhaps more symbolic animal names could have been chosen to highlight this distinction, but elephant and cheetah take too long to type, so roll with it.

In both applications, the UI includes a header that displays the user's email address and avatar when they are currently signed in. Since hippo is not formally connected to our server and database, we need another method of accomplishing two tasks:

  1. Determining whether or not a user is in a "logged-in" state. Have they recently been authenticated by our application?

  2. Fetching the user specific information for our UI- the user's email address and avatar.

Lucky for you and I both, JWT or JSON Web Token or JavaScript Object Notation Web Token (for those not into the whole brevity thing) provides a perfect specification to handle these tasks. JWT is a way to transfer JSON between two parties, signed and secured with a secret or key pair, so the data is verifiable and valid.

In the context of our applications, upon successful authentication of a user, rhino can store a JWT in local storage, then hippo can read this token and pass it back to our application in the authorization header of an API request, receiving the user's information in the success payload.

Application Setup

Going into construction specifics is beyond the scope of this article, but briefly, rhino is a Rails application tied to a PostgreSQL database. There is a user model, and the application leverages the Devise gem for it's user based authentication strategy. Rhino is based on Slining, Vaporware's Rails application starter that includes many standard defaults and assumptions.

Both applications utilize the Bootstrap framework for front-end styling. Hippo is far simpler in it's construction- a static HTML page tied to a local JavaScript file.

The applications are available on GitHub for demonstration at vaporware/rhino and vaporware/hippo. The base applications, prior to implementing JWT authentication, are available in their respective master branches, and the complete applications in their jwt branches. Follow along if you'd like.

Creating the JWT

We'll start with rhino, which will run point for the majority of the JWT strategy. To work with JWT, we can use the jwt-ruby gem- a pure ruby implementation of the JWT specification.

Add the following to your Gemfile.

gem 'jwt'

And run bundle install

Next, we'll create a JsonWebToken class in lib/json_web_token.rb that will encode and decode provided tokens based on a Rails secret key.

class JsonWebToken
  def self.encode(payload)
    JWT.encode(payload, Rails.application.secrets.secret_key_base)
  end

def self.decode(token) return HashWithIndifferentAccess.new(JWT.decode(token, Rails.application.secrets.secret_key_base)[0]) rescue nil end end

We'll also need an initializer to load the JsonWebToken class in config/initializers/jwt.rb:

require 'json_web_token'

As Devise is based on Warden, we can implement a callback after a user is set during the authentication cycle. Warden hooks need to be required when the application boots, so we'll add these to the Devise initializer in config/initializers/devise.rb.

Warden::Manager.after_set_user do |user,auth,opts|
  auth.cookies[:jwt_access_token] = { value: JsonWebToken.encode({user_id: user.id}) }
end

Warden::Manager.before_logout do |user,auth,opts| auth.cookies.delete :jwt_access_token end

Additionally, we can use a Warden callback to delete the previously set access_token when a user signs out.

Try logging in with a test user account. As you'll see in the developer tools application inspector, our newly created jwt_access_token is present with a properly formatted, three-part JWT value.

token set on login

Consuming the JWT

Briefly, to share user specific information with our client services, we'll need to authenticate API requests to the app against our JWT strategy.

In app/controllers/application_controller.rb we'll create an authenticate_request! helper method that can be called as a before_action on any controller method that we want to authenticate.

protected
  def authenticate_request!
    unless user_id_in_token?
      auth_error
      return
    end
    @current_user = User.find(auth_token[:user_id])
  rescue JWT::VerificationError, JWT::DecodeError
    auth_error
  end

private def auth_error render json: { errors: ["Not Authenticated"] }, status: :unauthorized end

def auth_token @auth_token ||= JsonWebToken.decode(http_token) end

def http_token @http_token ||= if request.headers["Authorization"].present? request.headers["Authorization"].split(" ").last end end

def user_id_in_token? http_token && auth_token && auth_token[:user_id].to_i end

With our method to properly authenticate requests against a JWT in place, we can implement the API for serving the user specific UI component information to our client service, hippo.

To handle incoming requests from hippo for UI component information, we'll create app/controllers/ui_components_controller.rb where we'll have a navbar action.

class UiComponentsController < ApplicationController

def navbar respond_to do |format| format.json do render json: navbar_template.to_json end end end

private

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">navbar_template</span></span>
  render_to_string<span class="token punctuation">(</span><span class="token punctuation">{</span>template<span class="token punctuation">:</span> <span class="token string">'application/_navbar'</span><span class="token punctuation">,</span> layout<span class="token punctuation">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span> formats<span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token symbol">:html</span><span class="token punctuation">]</span><span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token keyword">end</span>

end

And, add the corresponding entry in config/routes.rb.

get "/navbar", to: "ui_components#navbar"

Hit the navbar endpoint at localhost:5000/navbar.json. You'll see our navbar template represented as a string- currently in the state before a user has logged in, so no user email or avatar information present.

Let's add our authentication helper to app/controllers/ui_components_controller.rb and see how the response changes.

class UiComponentsController < ApplicationController

before_action :authenticate_request!

After adding the authentication helper, and resending the request to /navbar.json, our response is returned status 401 Unauthorized, with the message {"errors":["Not Authenticated"]}. Perfect. No JWT present in the authorization header, no user data granted.

Sending the JWT

Until this point, we've been working with rhino, our server-side Rails application. The next phase of our project will involve hippo, our static landing page. Hippo is availble on GitHub at vaporware/hippo. The initial version prior to implementing JWT is available in the master branch, and the finished version in the jwt branch.

Hippo is built using the Jekyll static site generator. To serve the project locally, run jekyll serve in the project's root directory.

The first step in calling rhino with our JWT authorization header, is to pull the JWT from browser's local storage. Ensure the JWT is present by logging in to rhino, and checking the application inspector. With the JWT present in the browser, let's script.

Create navbar.js in assets/js.

We'll need to add the script manually to includes/head.html as hippo has not implemented an asset pipeline.

<script type="text/javascript" src="{{site.url}}/assets/js/vendor/bootstrap.min.js"></script>

<script type="text/javascript" src="{{site.url}}/assets/js/navbar.js"></script>

Back in assets/js/navbar.js add the function getCookie() that will search the document's availble cookies to match a given string and return the cookie's value. We'll call this function and store the result in a variable called token.

function getCookie(key) {
  var keyValue = document.cookie.match('(^|;) ?' + key + '=([^;]*)(;|$)');
  return keyValue ? keyValue[2] : null;
}

var token = getCookie("jwt_access_token");

With the JWT pulled from the browser, we can make our call to rhino using jQuery's ajax function.

var request = jQuery.ajax('localhost:5000/navbar', {
  type: 'GET',
  headers: {
    'Authorization': "Bearer " + token
  },
  success: function(result){
    console.log(result);
  },
  error: function() {
    console.log("error!");
  }
});

On the first try, we are met with a CORS error. We'll need to define a CORS policy within rhino before we'll be allowed to call across different domains- in our case localhost:5000 and localhost:4000.

We can define a CORS policy via Rack middleware, using the rails-cors gem.

Add gem 'rack-cors', :require => 'rack/cors' to your Gemfile and run bundle install.

Now, we can add a CORS policy to config/application.rb.

config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:4000'
    resource '*', :headers => :any, :methods => [:get, :delete, :options]
  end
end

Restart the rails server and return to hippo, and let's try the ajax call again. Success! 200 a-OK.

Placing the Shared Component

The last step in presenting our shared component is actually placing it in the DOM. In includes/header.html there is a header tag, to which we'll assign id="shared-header-wrapper". This is the landing zone for our incoming navbar data.

In assets/js/navbar.js we'll create a function called setHeader() that will leverage jQuery to swap our shared-header-wrapper html with our returned navbar.

function setHeader(header) {
  if (header != null) {
    $('#shared-header-wrapper').html(header);
  } else {
    $('#shared-header-wrapper').html("");
  }
}

Remove the console logging from the ajax request success callback, and add the call to setHeader().

var request = jQuery.ajax('http://localhost:5000/navbar', {
  type: 'GET',
  headers: {
    'Authorization': "Bearer " + token
  },
  success: function(result){
    setHeader(result);
  },
  error: function() {
    console.log("error!");
  }
});

Reloading the hippo root page, we should now see our shared navbar in the header location. The user email address and avatar will be the same as on rhino.

To review, when a user logs in to our primary application, rhino, a JWT is created. The JWT encodes a reference to the user, and is saved in the user's browser. If and when the user browses to our secondary application, hippo, the JWT is pulled from the browser's cookies. An API call is made to rhino to request component information with the JWT added to the authorization header. Rhino then decodes the JWT, using the supplied user identifier to retrieve the appropriate information, and return it to hippo in the success payload. Of course, this is a fairly trivial example, but you can imagine that this pattern can be extended to other purposes.

To learn more about the JWT specification, the introduction page on Auth0's site JWT.io provides a great overview of the methodology.

Vaporware

Related Insights