9

Long time lurker, first time poster here.

There are many good guides and resources about JWTs and how and where to store them. But I'm running into an impasse when it comes to securely storing and sending a JWT between a ReactJS/Flux app running on a Node server and a completely separate Rails API.

It seems most guides tell you to just store the JWT in local storage and pluck it out for every AJAX request you make and pass it along in a header. https://stormpath.com/blog/where-to-store-your-jwts-cookies-vs-html5-web-storage/ warns against this, however, since local storage is not secure and a malicious person could access that token. It recommends storing it in the cookie instead and just letting the web browser pass it along with each request.

That sounds fine to me since from what I understand cookies get conveniently sent along with every request anyway. It means I can just make AJAX requests from my ReactJS app to my Rails API and have the API pluck it out, check it, and do it's thing.*

The problem I'm running into is my Node application doesn't set a cookie from the response it gets back from the Rails API even though the Rails API (running on localhost:3000) returns a Set-Cookie header and sends it back to the ReactJS/Node app (running on localhost:8080).

Here's my login controller action on my Rails API side:

class V1::SessionsController < ApplicationController
  def create
    user = User.where(email: params[:user][:email]).first!
    if user && user.authenticate(params[:user][:password])
      token = issue_new_token_for(user)

      # I've tried this too.
      # cookies[:access_token] = {
      #     :value => token,
      #     :expires => 3.days.from_now,
      #     :domain => 'https://localhost:8080'
      # }

      response.headers['Set-Cookie'] = "access_token=#{token}"

      render json: { user: { id: user.id, email: user.email }, token: token }, status: 200
    else
      render json: { errors: 'username or password did not match' }, status: 422
    end
  end
end

The gist of it is it takes an email and password, looks the user up, and generates JWT if the info checks out.

Here's the AJAX request that is calling it from my Node app:

$.ajax({
    url: 'http://localhost:3000/v1/login',
    method: 'post',
    dataType: 'json',
    data: {
        user: {
            email: data.email,
            password: data.password
        },
        callback: '' //required to get around ajax CORS
    },
    success: function(response){
        console.log(response);
    },
    error: function(response) {
        console.log(response);
    }
})

Inspecting the response from the Rails API shows it has a Set-Cookie header with a value of access_token=jwt.token.here

Screenshot: Chrome Dev Tools Inspector Screenshot

However, localhost:8080 does not show any cookies set and subsequent AJAX calls from my Node/React app do not have any cookies being sent along with them.

My question is, what piece(s) am I misunderstanding. What would I have to do to make storing JWTs in cookies work in this scenario?

A follow-up question: assuming storing the JWT in a cookie is not an option, what potential security risks could there be with storing the JWT in local storage (assuming I don't put any sensitive info in the JWT and they all expire in some arbitrary amount of time)?

*this may be a fundamental misunderstanding I have. Please set me straight if I have this wrong.

Side-notes that may be of interest:

  • My Rails API has CORS setup to only allow traffic from localhost:8080 in development.
  • In production, the Node/React app will probably be running on a main domain (example.com) and the Rails API will be running on a sub domain (api.example.com), but I haven't gotten that far yet.
  • There's nothing sensitive in my JWT, so local storage is an option, but I want to know why my setup doesn't work with cookies.

Update elithrar submitted an answer that worked:

I needed to modify my AJAX request with xhrFields and crossDomain as well as tell jQuery to support cors:

$.support.cors = true;
$.ajax({
    url: 'http://localhost:3000/v1/login',
    method: 'post',
    dataType: 'json',
    xhrFields: {
        withCredentials: true
    },
    crossDomain: true,
    data: {
        user: {
            email: data.email,
            password: data.password
        }
    },
    success: function(response){
        console.log(response);
    },
    error: function(response) {
        console.log(response);
    }
})

And I added credentials: true and expose: true to my Rack Cors configuration on my Rails API (the * is only for my development environment):

config.middleware.insert_before 0, 'Rack::Cors' do
allow do
  origins '*'
  resource '*', :headers => :any, :methods => [:get, :post, :put, :path, :options], credentials: true, expose: true
end

end

Diasporism
  • 91
  • 4
  • 1
    https://quickleft.com/blog/cookies-with-my-cors/ - you need to explicitly allow cross-domain cookies. Note that you cannot read/write to that cookie from your client as the browser will still enforce the same-origin policy (http://stackoverflow.com/a/14472492/556573). The JWT you store in the cookie is therefore completely opaque to the React client. – elithrar Oct 20 '15 at 02:52
  • This worked and I'll update my answer to reflect this. Thank you! – Diasporism Oct 20 '15 at 03:46
  • Saying "local storage is not secure and a malicious person could access that token" is a bit of a misnomer. It depends on context. A hacker would need to conduct an XSS attack to be able to retrieve your token. React, though, for example, is generally safe from XSS. It's really only not safe when you use something like eval() in render or DOM elements like this one: https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml. – colemerrick Jul 06 '18 at 18:07
  • What you're doing here, in fact, is arguably way less safe then going with the localStorage approach, and placing your JWT in request headers. – colemerrick Jul 06 '18 at 18:09

0 Answers0