CORS


Ok.

Our goal is to provide services by which other people may build webapps. The idea is, you GET something or POST something, and get back some JSON. This then goes to any number of client-side frameworks that are happy to work with JSON.

No probs, so far.

Oh, and of course I need to authenticate.

Ha ha ha! You poor fool! Welcome to the wonderful world of CORS. There’s rather a complicated dance you have to do, primarily because the world is full of bastards trying to steal your credit card details. It’s complicated for the same reason that banks have bullet-proof glass and safes all over the shop.

Now, I’m using Angular, and they seem to like this thing called a JSON Web Token – JWT. You log in to your server, you get back a JWT, and you include that in a HTTP header. Of course, this has to be over https or it’s game over.

Protocol

JWTs have a structure, but at the moment on my dev box the JWT is just the user name – no password, no nothing. Not sure how it’s supposed to be done, but on my dev box (I stress this is nowhere near prod), I’ll pass it back and forth in an http header named nsl-jwt.

Back end

Our grails app is using Shiro. I build a credentials object named JsonToken, whose job it is to hold the token that gets put in the nsl-jwt header. It has two constructors: one that takes a string (the header content), and one that takes a Shiro subject. The job of the one that takes the shiro subject is to build the token for a given user. Maybe this should be a builder method.

In any case, I have a security realm named JsonTokenRealm, which knows how to interpret JsonToken credentials objects. This realm is a spring bean, obviously, which holds a reference to the LDAP security realm bean by injection. It implements authenticate, but everything else is deferred off to LdapRealm. (In principle – I see it’s commented out at present).

So that’s yer back-end.

Front end

For the front end, my angular app makes a call to signInJson. This takes a username and password, and returns a JSON packet with the JWT (and potentially MOTD, etc). This JWT needs to be included into subsequent calls, in the nsl-jwt header.

To do this, you call the angular module’s config method, passing it a method that has an injectable $httpProvider parameter. This method pushes a function into the interceptors, that returns a map of functions that respond to the various JSON request lifecycle events: in this case, request. That method stuffs the JWT into nsl-jwt. At this stage, I am just using ‘p’, because I will figure out later how the injected request function is supposed to get the application scope. (p is a user name in the LDAP server running on my DEV box).

Simple.

app.config(['$httpProvider', function($httpProvider) {
  $httpProvider.interceptors.push(
    function() {
      return {
        'request': function(config) {
          config = config || {};
          config.headers = config.headers || { };
          config.headers['nsl-jwt'] = 'p';
          return config;
      }
    };
  });
}]);

And there’s yer front-end.

Not so fast

Oh, did you think that was it? Of course it isn’t. Nothing works.

The client app identifies objects entirely by their ‘mapper’ id. The job of the mapper is to convert these abstract URIs into concrete URLs. It’s job is to know what servers (‘shards’) own which URIs, and to generate a 303 redirect.

Now, we have already addressed the host cross-origin issue. There was a grails plug-in, which doesn’t seem to work. So I added a hard

response.addHeader('Access-Control-Allow-Origin', '*')

To the fun bit. But this isn’t enough when you are also sending along suspicious http headers. Oh noes! So I added a

response.addHeader('Access-Control-Allow-Headers', 'nsl-jwt')

Then firefox started to whine about a ‘preflight check’. Turns out that when your JSON request has weird stuff, the CORS spec makes you send an OPTIONS request. The mapper doen’t respond to these (Why would it? After all, what kind of weenie sends obscure HTTP reqests?) SO I wrote some stuff.

Aaaaand … it doesn’t work. Firefox sends OPTIONS, gets back the access control gear, sends the GET, gets back the 303, then stops. The angular http provider don’t follow redirects when there’s weird complicated CORS (works fine if I don’t include that nsl-jwt header. Maybe there’s something I’m missing.

Three possible fixes

Manually handle redirect

Rig up the ‘refresh me’ code to do its own redirect loop

Distinguish between mapper and other ids

I think this is the simplest fix. We only need the JWT when we are doing an edit, and the edit URLs all have a specific structure. The request hook just needs to look at the URL being requested to decide if the JWT needs to be there.

Read the goddamn CORS spec

Sigh. And this is probably the right way to do it. Spec is at https://www.w3.org/TR/cors. As I am old-school, I will need a printer and my reading glasses.


Update

Well, it looks like CORS just plain doesn’t do redirects when life becomes complex. Section 7.1.4 (simple request) has explicit handling of redirects. Section 7.1.5 (cross-origin with preflight) states that either in the preflight step or the actual request step, if the response code is anything outside the 2XX range, then error.

I suppose it’s an application’s job to know what servers it’s talking to and which need passwords, so option 2 (if it’s an edit, then include the password) becomes reasonable.

I’ll roll back my mods to the mapper, whip out the code on the andgular $http.request hook, and have the controllers explicitly include the JWT for those JSON requests that are edits.

Advertisements

One Response to CORS

  1. lporiginalg says:

    CORS is a real pain, especially when you have clients using IE9- ;)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: