We were unable to load Disqus. If you are a moderator please see our troubleshooting guide.
Nice write-up!I went for session cookies in a very lazy time-pressured "aha" moment some years ago. It's been working in production for 3-4 years on a well used site without issue. It wouldn't be appropriate for a back-end API like a payment gateway where there's no user with a browser to send to a log-in screen, but for normal web pages, and especially carving js apps out of / on top of an existing site, it's extending what we have instead of starting again.
Handling 401s well is important for the user's experience. They won't happen often (though more often than I expected), but really do break everything if you're not careful. Getting a good authentication abstraction library for Vue or Ember or whatever you are using should help with a lot of the boring parts. You'll probably need to define some extra strategies/rules for this cookie session approach, but if it's anything like in ember-simple-auth they're so simple it feels like cheating, because the Rails app is doing all of the hard work and you just need the js part to spot a 401 and handle logging in and retrying whatever it was doing before. This stuff is all rather boring or frustrating when you just want to get your app finished, but network requests are a big deal, and having to deal with this kind of thing is one of the prices of switching away from server-side rendering to a distributed system.
On the security side I think code injection is still a danger. If someone does smuggle js into your js app they'll be able to read your CSRF cookie and make ajax requests using your logged-in http session, just like your own code does?
Thanks, Nick. It's good to hear that this has been working well for you, and we appreciate the tips about handling 401s.
In terms of code injection, indeed there is still a danger as an XSS vulnerability can circumvent CSRF defenses.
I was just contemplating the same thing. We consume a single API using our mobile apps and the web app. It seems unnecessary to gen and store a token in the browser when sessions come baked in. Good article!
Thanks for this, Mike. The last couple of SPA style apps we've started, we've taken this approach. I just think it makes a lot of sense. I haven't personally heard of anyone getting bitten by JWT/token based auth, but if you're not accessing the API from a mobile app, it seems like using the old standard session/cookie auth approach is more secure and easier to work with. When security is concerned, stick with "boring old technology".
Well said, Elliot! While we often venture into projects using shiny new technology, we also appreciate the stability of boring old technology. 😀
I have one question that doesn't seem to be answered clearly on any blog post so far. What if I have a single consumer for my API. For instance, I've got a Rails API that gathers cryptocurrency prices every 5 minutes. I then create a single consumer website that uses this API. In this case, I wouldn't be able to use the technique described above. If I had a single user for my Rails API, I'd have to pass in the username/password through the Vue.js application. Anyone who viewed the source code could get this single user's credentials and use my Rails API on their own website. Do I just restrict calls to my API via the referrer in this case so that only the application hosted on my domain name is able to call the API?
Great write up and excellent overview. I ended up taking the opposite approach though. While rails does have nice CSRF protection, in my instance it limited me. My app is similar to the one you describe above. Since the web app was separate from the backend api - I wanted to pre-render the app's html/js/css and store it as static html on AWS S3. If I could do that, then I could run my API using API Gateway and Lambda functions and my cost to run the whole thing would be less a $1.00/month. I started off really wanting to use HttpOnly cookies, I had read a decent amount by tptacek on HN, but in the end decided to use localStorage.
In short: storing the token in HttpOnly cookies mitigates XSS being used to get the token, but opens you up to CSRF, while the reverse is true for storing the token in localStorage. Therefore, since each method had both an attack vector they opened up to and shut down, I perceived either choice as being equal. The only thing I've read so far that suggests one is better than the other is that HttpOnly cookies have been around longer and are therefore more battle tested.
Hope that helps - thanks for putting this together -
Thanks for your candid and thoughtful reply, Neal. We respect your decision, and appreciate that you took the time to explain it.
Super helpful! We're working through the SPA Pragmatic Studio course as well and ran into this problem. Just to clarify - if our goal is to build out a SPA that connects with an existing Rails App on EC2 - is it possible to use cookie based auth or are we stuck with tokens? My suspicion is that a pre-built VueJS hosted on AWS should still set cookies since the JS request is sent to our EC2 server (which then responds with cookies) but this comment just had me questioning this approach.
thank you for this article. I am appreciate it
I have a question: As I understand it we are: (i) setting a CSRF-TOKEN within a cookie, and (ii) passing that csrf cookie to the rails-app - then won't the CSRF-TOKEN within the cookie be subject to the CSRF attacks just like with the session cookie?
Hi Ben. Yes, all cookies are vulnerable to CSRF attacks. However, the cookie containing the CSRF-TOKEN is only used by the client to set the X-XSRF-TOKEN header. So passing a compromised CSRF-TOKEN cookie to the Rails app won't have any negative effect.
The Pragmatic Studio And if my API is in another domain? My cookies are being sent but the header is not.ex api: myapi.comapp: myapp.comWhat would you suggest?
Since cookies can't be shared across top-level domains, you would need to use a token-based authentication approach in that scenario.
The Pragmatic Studio is this also true in case of a subdomain? I am trying to use www.mysite.com and api.mysite.com names and I run into issue where site cookies are simply not created in the browser. I am following steps in your article in my setup. I've described my issue in more details here: https://stackoverflow.com/q... Thank you!!!
How about just having one endpoint (for example /users/token) that allows cookies?
That way when the browser refreshes we can just fetch the token from there and save it in memory instead of using the localStorage.
Is there some way to make this work with a progressive web app?I would like to use background sync to store a request in indexeddb when the user is offline and posting a form fails. The background sync api will try to send the whole request multiple times for a limited time until it succeeds sending the request to the server. But when the user closes off the browser the user is signed out and the csrf token becomes invalid. Is there a way to make the approach suggested here work for a PWA?
Off the top of our heads, we can't think of a way to make this work with the CSRF token approach. There may well be a solution, but it would take some research/experimentation to figure that out.
My understanding is that you can no longer use this approach and have a different domain for your api (like app.mything.com and api.mything.com), due to Safari's Intelligent Tracking Prevention.
As long as the cookie's domain is set to .example.com, any subdomain or top level domain at example.com (ie, api.example.com, www.example.com) will share the cookies. I don't think Safari's ITP will affect this - it's only for different top level domains.
That's my understanding too. Sean is right I think, but that's no help if your js widget is on multiple domains as an embed.
Nice article. I'm current going with the same approach on an app I'm building. However, writing rails request specs is impossible because they don't allow access to the request session in tests. Any workaround for this?
You know I noticed when using this approach, that for some reason I can't change the session_store in Rails.
I tried creating an intializers/session_store.rb file with the following:
Rails.application.config.session_store :cookie_store, key: '_my_app_session'
But the key name on the session would not change.
It only changed after I set api_only=false in the application.rb
Does anyone have a solution to this other than changing apy_only to false?
You need to add this to application.rb file instead of intializers The options are not passed to thee store from initialisers when rails app is in api only mode. Ref: https://edgeguides.rubyonra...
This doesn't work
Hey, great post, thanks!.
One question, I couldn't find it in the comments but If cookies are HttpOnly, how is able Axios to get the CSRF token from cookies to pass it again as a header? I can't see the header in the non-GET requests, and I think is because of that.
Just tried you method with setting form_authenticity_token via cookies - and it seems not working as expected.Because token is base64 encoded value, with special chars included. These chars are encoded to equivalents like %3D%3DCould you suggest any solution for this without manual frontend decoding?
I would like to have a user login to both rails "regular non-api web-app" and the rails "api" at the same time. Could this approach be used to do that?
> localStorage is vulnerable to Cross-site Scripting (XSS) attacks [...] Using a Rails session cookie sidesteps the potential for XSS attacks.
So using Rails sessions doesn't prevent an XSS attack, it just slightly mitigates it by forcing the attacker to use of a different way to steal your data. So the decision of choosing Rails sessions over localStorage specifically because of XSS attacks doesn't sit quite right with me since either way once an XSS attack happens, it's game over.
I can understand that choosing Rails sessions may be more convenient because it uses familiar Rails built-ins, or because of personal preference, or as Nick Rutherford mentioned when you're making a new JS app on top of an existing Rails app and reusing the authentication that is already built vs starting again.
In any case thank you for the write-up, as if one does choose to go with Rails sessions, the instructions are explained very well.
During the implementation of this, I realised that the configuration in config/environments/*.rb for ActionController::RequestForgeryProtection are not set for the ApplicationController. I realised this when testing with RSpec request spec when, forgery protection was supposed be disabled for the test environment, but was not.
This is because this the module ActionController::RequestForgeryProtection is included in the concrete controller class after the application is configured so the configuration is not applied.
I fixed this using an initializer that reapply the configuration. I made a GitHub gist.
This blog is very informative, We also have this same blog in detail, this will help you to know more about RAILS 6 & 7 API Authentication with JWT