Some of you might have tried to run my tutorial code from my previous series of blogs about securing REST services using Spring Security and writing an AngularJS-based web client for them. You checkout the web-side and server-side code from GitHub, you run everything and you access the web client by duly typing “http://127.0.0.1:8080” in your browser and you get the application running.
You then start testing the open, non-secured services and all seems to go well! Then you decide to login so you can test the secured services. You type “user/user” as the username and password, click on the “Login” button and… you get a 403 Forbidden status in your browser’s console. The login has failed: the server blocked your request to login!
If this is happening while running my tutorial code, you might also get an additional message courtesy of the web client: “The obtained CSRF token was either missing or invalid. Have you turned on your cookies?“. So you make sure that cookies are enabled. They are!
Next step: you take a closer look at the browser’s console, and you notice that the CSRF token the code is sending to login is actually null. On my tutorial, I actually even perform a check and log the token being sent: “Extracted the CSRF token from the cookie: null“!
Might it be a bug in the client code that extracts the CSRF token from the returned cookie?
So you push the investigation further and look at the exchanged network messages, especially the OPTIONS requests to obtain the CSRF token and the JSESSIONID cookies. And you can see that the OPTIONS request is indeed doing that “Set-Cookie:CSRF-TOKEN=507c002f-b7a8-43f7-be89-e0ef36881203;Path=/“.
Aha! So the cookie is actually sent to the client, but the client is not storing it to send it back at login! It’s a bug! A bug I tell ya!
Well, it’s not the code. Give me some credit, I tested that thing! 😉
However I didn’t expect some of you (including one of my colleagues) would type “127.0.0.1” in the browser. I expected you to type “localhost“!
If you go and look at the web client code, you’ll probably find that the REST queries to the server are made using “localhost” in the web client’s code, not “127.0.0.1“. That means the REST requests are being made as a different domain than the domain referenced by the browser’s URL.
In other words: although the browser is accessing the application from “127.0.0.1“, it is requesting its data (including it’s cookies!) as “localhost“.
Now, I had trouble gathering precise information on this, so what follows is my understanding of how it happens (feel free to correct me in the comments).
The web client does an AJAX request from port 8080 to the server on port 8081. By configuring CORS on the server side and ensuring that AJAX requests are made using the “withCredentials” option (configured globally with $httpProvider), we have managed to have our server exchange data between port 8080 and port 8081, and that’s including the cookies information. This is why you have a response that includes the “Set-Cookie” fields in its header. This is also where the CORS action stops.
At this point, the browser sees the “Set-Cookie” fields in the server’s response and understand that it has to store those cookies for the same domain that made the REST query, namely “localhost:8080“.
But the URL in the browser says “127.0.0.1” and therefore the cookies, which were meant for “localhost“, are prevented from being stored for “127.0.0.1“.
This makes sense from a security perspective: imagine the damage one could do if a cookie emitted for B’s domain could “invade” A’s domain…
So to wrap this one up: just don’t mix localhost and 127.0.0.1 when developing in your local environment 😉