UPDATE: Here's my new blog

I CAN HAS ENGINEERING

By Amr Numan TamimiTwitterGoogle+

Building a Web Client like Pros

amrnt 2 years ago CommentsXHR coffeescript cors javascript ruby api authentication


Updates at the bottom

In this post, I will guide you with some tips and tricks on how to build a web application with the top of your own API.

So, let’s say you have an Awesome Web Startup and you want to build it the modern way. What I mean is: You have an API and a User Interface “Javascript application” that communicates with the API . We can find dozens of websites that uses this technique, for example Twitter, Foursquare, Quora and much more.

Lets say you are having myawesomeapp.com as a front end for your users. Once they signed in, they grant authentication to access the API. Then you want to make Ajax requests to other server than the domain that hosts the client application e.g. api.myawesomeapp.com. This leads lots of the newbie startups to setup the API server in the same domain of the client application to avoid the limitation of XmlHttpRequest object in the web browsers: myawesomeapp.com/api. This problem is caused because Same Origin Security Policy of browsers, which prevents a page from accessing data from another server. I will not talk about the Same Origin Security Policy issue, you can read about it here.

The Authentication

Leading startups like Twitter, are using their own public API in their client applications which is in most cases hosted on a different server than the application. I will take Twitter as a case for how they make call requests to their API server api.twitter.com and the way of the authentication they adopt for communication between twitter.com and api.twitter.com. Thought, Twitter is using oAuth to authenticate users for their API externally.

The easy way of that is to grant them the authentication by sharing same cookies of the front end application. By being signed in and authenticated to the client application, the API server will be accessible to the user via the client.

The Twitter way

For each user that signed up for Twitter, the system generate a unique token for each user (in case if Twitter is an oAuth client: access_token for the internal oAuth application). This token is being saved in the cookies with key auth_token. The token is used to grant the access for the user for to the API.

Talking about granting users’ access to the API could be done by many ways, cookies as Twitter does, sending the token as a parameter in the requested url, or sending the token via a header. Since this token is a randomly generated and created, beside it is unique token cross all users, it leads us to be hard to guess string. So on the API side, we find the user with this unique token and give her the access to the API.

How I do it

I use Rails for building my Javascript Client and Grape for the API. In the rails application I use plataformatec/devise for authentication, which is built on the top of hassox/warden. So in the API I can access the user via env["warden"].user.

We also share the cookies of the rails app with the API by:

# in config/initializers/session_store.rb
App::Application.config.session_store :cookie_store, key: '_app_session', :domain => :all

In the API’s config.ru

# THE_LONG_SECRET is from config/initializers/secret_token.rb in the rails app
use Rack::Session::Cookie, key: "_app_session", secret: "THE_LONG_SECRET", domain: ".domain.ltd", httponly: true

So now the cookies are shared by the two apps: the api and the client.

Once the user signed in, the authentication can be checked through env["warden"]. But we want to focus now on how to authenticate them throw the authentication_token.

In Devise, we have a great feature that we can generate a unique token for each user, just add :token_authenticatable to devise modules list in your model (User):

devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable, :token_authenticatable

Make sure that Devise is also ensure that each user has an authentication_token with adding before_save :ensure_authentication_token to the model.

Also, you can add the user’s authentication_token to cookies with key auth_token by a Warden callback (And another callback to remove it from cookies after signing out):

Warden::Manager.after_set_user do |user, auth, opts|
  auth.cookies["auth_token"] = {
   value: user.authentication_token,
   domain: :all,
   httponly: true
  }
end

Warden::Manager.before_logout do |user, auth, opts|
  auth.cookies.delete("auth_token", {domain: :all})
end

In the next snippet of code, I show you how we authenticate users to the API. So, as you see we grant the authentication via warden and authentication_token via cookies, headers, and as a parameter.

module ApiAuth
  # Finding auth_token in the HTTP_COOKIE.
  # Will loop throw all cookies to find it.
  # Returns the value of auth_token when find it.
  # Else returns nil.
  def auth_token_with_xml_http_req_via_cookies
    if env["HTTP_COOKIE"]
      env["HTTP_COOKIE"].split(/; /).each do |cookie|
        k, v = cookie.split(/\=/, 2)
        if k == "auth_token" and env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest" # We check also if the request was made by an Ajax request
          return v.to_s
        end
      end
    end
    return
  end

  def auth_token_via_headers
    if env["HTTP_X_AUTH_TOKEN"]
      return env["HTTP_X_AUTH_TOKEN"].to_s
    end
    return
  end

  def warden
    env["warden"]
  end

  def authenticated
    if warden.authenticated?
      return true
    elsif auth_token_with_xml_http_req_via_cookies and User.where(:authentication_token => auth_token_with_xml_http_req_via_cookies).first
      return true
    elsif auth_token_via_headers and User.where(:authentication_token => auth_token_via_headers).first
      return true
    elsif params[:auth_token] and User.where(:authentication_token => params[:auth_token]).first
      return true
    else
      error!({ code: 401, message: "Unauthorized." }, 401)
    end
  end

  def current_user
    warden.user ||
      User.where(:authentication_token => auth_token_with_xml_http_req_via_cookies).first ||
        User.where(:authentication_token => auth_token_via_headers).first ||
          User.where(:authentication_token => params[:auth_token]).first
  end

  def authenticated_user
   authenticated
   error!({ code: 401, message: "Unauthorized." }, 401) unless current_user
  end
end

So, in the api you can ensure if the user is authenticated by authenticated_user method, and you can access the user object by current_user.

Client Application and the Ajax Requests to the API

Now, the beautiful part, you are on the client side myawesomeapp.com and want to make Ajax requests to the API api.myawesomeapp.com that returns JSON object.

I’m using jQuery as a javascript library, and here is a simple GET request:

$.get("http://api.myawesomeapp.com/1/me")

Ops!

XMLHttpRequest cannot load http://api.myawesomeapp.com/1/me. Origin http://myawesomeapp.com is not allowed by Access-Control-Allow-Origin.

Solution

There are lots of technique to avoid this limitation of browsers (mentioned at the top of the post) you can read here.

The best working solution is to setup an iframe that served by the API domain and make requests to the API via it. I read lots of Javascript in the source code of Twitter and Foursquare and I’ve extracted a great piece of code to solve this.

First, set up a static html page receiver.html that served by the API server: api.myawesomeapp.com/receiver.html and contains this html code:

<html>
  <head><meta http-equiv="Cache-Control" content="public, max-age=31556926" /></head>
  <body><script>document.domain='myawesomeapp.com'</script></body>
</html>

Second, create an iframeManager in the client app (extracted from foursquare.com). Here’s a the coffeescript snippet of the iframe manager: (javascript version: https://gist.github.com/1555068)

iframeManager =
  xhrCallback: null
  iframeLoading: !1
  loadQueue: []

  addLoadCallback: (a)->
    if iframeManager.isLoaded() then a() else iframeManager.loadQueue.push(a)

  runLoadCallbacks: ->
    a() for a in iframeManager.loadQueue

  isLoaded: ->
    null != iframeManager.xhrCallback

   buildIframe: (apiServer) ->
    if not iframeManager.iframeLoading
      iframeManager.iframeLoading = !0
      a = document.createElement("div")
      b = "#{apiServer}receiver.html?parent=#{encodeURIComponent(window.location.href)}"
      a.innerHTML = """<iframe onload="window._tempIframeCallback()" id="receiver_iframe" tabindex="-1" role="presentation" style="position:absolute;top:-9999px;" src="#{b}"></iframe>"""
      c = a.firstChild
      window._tempIframeCallback = ->
        delete api._tempIframeCallback
        iframeManager.xhrCallback =
        if window.XMLHttpRequest and ("file:" isnt window.location.protocol or not window.ActiveXObject)
        then ->
          new c.contentWindow.XMLHttpRequest
        else ->
          try
            return new c.contentWindow.ActiveXObject("Microsoft.XMLHTTP")
        iframeManager.runLoadCallbacks()
      document.body.appendChild(c)

window.iframeManager = iframeManager

And here’s a snippet of the Api class that through it we will create requests to the API (javascript: https://gist.github.com/1555071)

At the bottom of the code you have to set the API domain and the API version: window.api = new Api "http://api.myawesomeapp.com", 1

class Api
  constructor: (apiServer, apiVersion) ->
    @iframeManager = iframeManager
    @apiServer = apiServer
    @apiVersion = apiVersion

  ajax: (req) ->
    if @iframeManager.isLoaded()
      req.xhr = @iframeManager.xhrCallback
      req.crossDomain = not 1
      req.dataType = "json"
      $.ajax req
    else
      self = @
      @iframeManager.addLoadCallback ->
        self.ajax req
      @iframeManager.buildIframe(@apiServer)

  request: (options) ->
    options.type = options.type or "GET"
    options.url = if options.url then "#{@apiServer}#{@apiVersion}#{options.url}"
    options.data = options.data or {}
    options.headers = options.headers or {}
    @ajax options

window.api = new Api "API_DOMAIN", API_VERSION # notice to change API_DOMAIN and API_VERSION

So now you have access to the api variable in the whole of the client application. To make requests to the API you have to use api.request() function.

Here’s some example how to make requests:

api.request({url: "/me"}) // this makes a GET request to http://api.myawesomeapp.com/1/me

You can use jQuery methods of $.ajax: done, fail, always

api.request({url: "/me"}).done(function() { alert("success"); }).fail(function() { alert("error"); }).always(function() { alert("complete"); });

We grant the authentication of these requests by the authentication_token that we save in the cookies.

if you have not authenticated via cookies you can use the header:

api.request({url: "/me", headers: {"x-auth-token": "xhg7t37vy3rvFS4jt4"}})

or by parameter:

api.request({url: "/me", data: {"auth_token": "xhg7t37vy3rvFS4jt4"}})

for POST requests:

api.request({url: "/articles", type: "POST", data: {...}})

for PUT and DELETE requests: we handle it the Rails way: make a POST request with _method parameter for the request type and don’t forget to add this line to config.ru in the API application use Rack::MethodOverride

api.request({url: "/articles/123", type: "POST", data: {_method: "put", …}})

api.request({url: "/articles/123", type: "POST", data: {_method: "delete"}})

Conclusion

I read this tweet yesterday, retweeted by a friend: Samer Abu Khait.

You know how to handle authentication and cross domain requests. It’s your time to craft your application the professional way!

Update

1- Dont forget to put this snippet you the top of your Client application

    <script type="text/javascript" charset="utf-8">
      document.domain = 'myawesomeapp.com';
      <!-- OR document.domain = '<%= request.host_with_port %>'; -->
    </script>

2- (Recommended step) You can drop using Warden in your application, and use only authentication via the cookie token, Also, I recommend to use CommonCookies rack middleware in case you dont want to set the domain manually (better for testing locally)

# THE_LONG_SECRET is from config/initializers/secret_token.rb in the rails app
use Rack::Session::Cookie, key: "_app_session", secret: "THE_LONG_SECRET", httponly: true
use Rack::CommonCookies
# dont for get to install `rack-contrib` gem and require "rack/contrib/common_cookies" in the top of API config.ru