Skip to content

Instantly share code, notes, and snippets.

@timup
Last active May 6, 2019 17:35
Show Gist options
  • Save timup/f2f028c3ca70447059ec474c5f177542 to your computer and use it in GitHub Desktop.
Save timup/f2f028c3ca70447059ec474c5f177542 to your computer and use it in GitHub Desktop.
Shared Component Blog Post

Shared Components with JWT Authorization

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 defered 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 seperate parts- our distinct services. The first, named "rhino", is a database driven Ruby on Rails application. The second component, refered 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 ficticious 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 acomplishing 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 spefication 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 sucessful 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 availble 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

    def navbar_template
      render_to_string({template: 'application/_navbar', layout: false, formats: [:html]})
    end
end

And, add the coresponding 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 compoent 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 retreive 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment