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
Written by Jose Raya
Related protips
5 Responses
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 :)
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).
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...
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
Thank you, this post was a lot easier to google than those github discussions!