We were unable to load Disqus. If you are a moderator please see our troubleshooting guide.

Nick Rutherford • 5 years ago

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?

The Pragmatic Studio • 5 years ago

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.

Thanks again!

Chad Wilken • 5 years ago

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!

The Pragmatic Studio • 5 years ago

Thanks, Chad!

Elliot Larson • 5 years ago

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".

The Pragmatic Studio • 5 years ago

Well said, Elliot! While we often venture into projects using shiny new technology, we also appreciate the stability of boring old technology. 😀

Praveen Angyan • 5 years ago

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?

Neal Griffin • 5 years ago

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 -

The Pragmatic Studio • 5 years ago

Thanks for your candid and thoughtful reply, Neal. We respect your decision, and appreciate that you took the time to explain it.

matt123456 • 5 years ago

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.

Ben • 5 years ago

Hi Mike

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?

rgds

The Pragmatic Studio • 5 years ago

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.

Ruan Kovalczyk • 5 years ago

The Pragmatic Studio And if my API is in another domain? My cookies are being sent but the header is not.
ex api: myapi.com
app: myapp.com
What would you suggest?

The Pragmatic Studio • 5 years ago

Since cookies can't be shared across top-level domains, you would need to use a token-based authentication approach in that scenario.

Alik Kasman • 3 years ago

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!!!

Alexandre Pestana • 5 years ago

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.

Sander Groen • 4 years ago

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?

Thanks

The Pragmatic Studio • 4 years ago

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.

danlucraft • 4 years ago

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.

sean • 3 years ago

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.

SeaWill • 3 years ago

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.

Nikhil Fadnis • 3 years ago

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?

Nathan • 3 years ago

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?

Vivek Marakana • 3 years ago

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...

Nathan • 3 years ago

This doesn't work

Devsfutter • 3 years ago

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.

terryprogetto • 3 years ago

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%3D
Could you suggest any solution for this without manual frontend decoding?

David • 3 years ago

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?

Jerome Dalbert • 2 years ago

> localStorage is vulnerable to Cross-site Scripting (XSS) attacks [...] Using a Rails session cookie sidesteps the potential for XSS attacks.

If your app falls victim to an XSS attack, the attacker has full control over the application's code via JavaScript. It is true that compared to localStorage, using the HttpOnly Rails session cookie will prevent the attacker from reading authentication data. But the attacker still has full control, so nothing prevents them from say logging you out and intercepting every keystroke when you log back in, thus stealing your username and password. And of course since they have full control they can read any private data displayed on the website.

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.

Simon • 2 years ago

Nice article!

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.

Bluebash • 2 years ago

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