Last Updated: March 02, 2016
·
4.69K
· joseraya

CORS directive for Spray

In our current project we are using Spray to develop a RESTful API. We needed to enable CORS support for our javascript and I was unable to find an easy way to do so. What I wanted was:

A directive that can be used at the top most level of my routes that enables CORS support for all the routes contained on it. That means:

  • Respond to OPTIONS requests with the right headers only for the registered paths (that is, OPTIONS /something-that-does-not-exist should result in a 404, not a 405
  • Add the Access-Control-Allow-Origin header to every request

After some digging, I found out that spray has very good support for something like this.

For the first part, responding to OPTIONS, it is as easy as looking at the response, see if it is going to be rejected and, if the cause of the rejection is 'MethodNotSupported' that means that there is a route somewhere inside your CORS directive that would respond to this uri with a different method. That is, if the request was an options, we have to respond OK as it is a preflight request. We can even build the list of allowed methods for Access-Control-Allow-Methods from the list of rejections.

For the second part, all we need to do is to add the header to the response.

You can find the code here: https://gist.github.com/joseraya/176821d856b43b1cfe19

5 Responses
Add your response

Great project! Already using it in a QA env. I'm just curious about how would you handle spray authentication?

Something like:

cors {
pathPrefix ("api") {
authenticate (myAuthenticator) { user =>
path ("detail") {
get {
....
}
}
}
}
}

Since CORS 'preflight OPTIONS request' does not allow to set Credentials nor Authorization Header, I had to add an extra validation to your original code here (CorsSupport.scala, line 19 of your original code):

case Rejected(x) if (ctx.request.method.equals(HttpMethods.OPTIONS) && !x.filter(.isInstanceOf[MethodRejection]).isEmpty) || /extra validation/
(ctx.request.method.equals(HttpMethods.OPTIONS) && x.exists(
.asInstanceOf[AuthenticationFailedRejection].cause.toString == "CredentialsMissing")) => { ... }

You think there is a better way to do this? Thanks :)

over 1 year ago ·

Thank you very much for your comment. I did not run into this problem and I guess that it is because of the order in which I declared the directives:

cors { pathPrefix(...) { post { authenticate(myAuthenticator) { ... } } } }

In this case, the metod rejection happens before the authentication rejection and my original code works. It is my understanding that, in your case, as authenticate is declared before get it will reject the request before even looking at the method and, as you experienced, my code does not work because it does not find the MethodRejection (at least that's my guess).

over 1 year ago ·

Hi,

Nice implementation. I'm new to spray as such somethings may not be clear. Your cors directive seems to allow all origins from all domains.. I'm I correct? Isn't this a security issue?

Perhaps changing the first line in the code to accept an instance of List(AllOrigins) where method can be declared to include a List of concrete implementation of AllOrigins such as SomeOrigins. For improved security.
Then code use can be for instance:

cors(List(domain1.com, domain2.com){
PathPrefix('api'){
get{
.....more code here
}
}
}
Then again maybe I'm wrong could you please comment...

over 1 year ago ·

Hi,
I think that you are right and it will be more secure if you can restrict the list of possible origins although it might be a litte cumbersome to manage (for instance, changing your code or your configuration every time you want to add a new origin).
You can find a good discussion of the issue here: http://stackoverflow.com/questions/19322973/security-implications-of-adding-all-domains-to-cors-access-control-allow-origin

over 1 year ago ·

Thank you, this post was a lot easier to google than those github discussions!

over 1 year ago ·