Devise-JWT: Using cURL I can see an Authorization header. Using Axios/Fetch there is no Authorization header

1k Views Asked by At

I have a very typical Rails backend with Devise and Devise-JWT. When I make a raw cURL request to the endpoint /users/sign_in I can see in the headers that it is setting an Authorization header with a token.

When I do the same request on my React frontend (which is on a different port, so cross origin configuration is necessary) the only headers I'm seeing in the result are cache-control and content-type.

Now, I have installed the 'cors' gem. I have created an initializer called config/initializers/cors.rb and I have put this configuration inside:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
    allow do
        origins '*'
        resource('*',
            headers: :any,
            expose: ["Authorization"],
            methods: :any
        )
    end
end

Yet when I make the request in my React frontend, I see no Authorization header.

I can see that Rails is responding with a 302 redirect. Is this part of the problem? Do I need to configure devise to stop responding with a 302?

I am utterly lost as to what the problem could be.

If you observe the screenshot that follows, logging in the normal way via plain form login gives me an Authorization header, but if I do it via AJAX there is no header.

This image shows that there is an Authorization header when hitting the /users/sign_in endpoint normally with a non-AJAX request

This is the frontend (AJAX) code which doesn't give me any Authorization header, only "cache-control" and "content-type":

async function submitLogin(email, password) {
    let formData = new FormData();

    formData.append("user[email]", email)
    formData.append("user[password]", password)

    let result = await Axios.post(`${process.env.API_URL}/users/sign_in`, formData, {maxRedirects: 0})

    console.log(result)
}

the result:

enter image description here

1

There are 1 best solutions below

0
On

DISCLAMER: This is not a 100% answer to your question, but it will work for sure!


In the case devise-jwt is not mandatory in your project, I would suggest you to follow what I did in my Rails 6 / React template:

  1. Configure you project routes in order to override the Devise's sessions routes like I did in my template project
  2. Override the Devise's SessionController in order to make it responding with the JSON format and returning a JWT:
# frozen_string_literal: true

class SessionsController < Devise::SessionsController
  clear_respond_to
  respond_to :json

  def create
    super do |resource|
      if user_signed_in?
        resource.token = JwtWrapper.encode(user_id: current_user.id)
      end
    end
  end
end
  1. Add the JwtWrapper in app/helpers/jwt_wrapper.rb:
# frozen_string_literal: true

module JWTWrapper
  extend self

  def encode(payload, expiration = nil)
    expiration ||= Rails.application.secrets.jwt_expiration_hours

    payload = payload.dup
    payload['exp'] = expiration.to_i.hours.from_now.to_i

    JWT.encode payload, Rails.application.secrets.jwt_secret
  end

  def decode(token)
    decoded_token = JWT.decode token, Rails.application.secrets.jwt_secret

    decoded_token.first
  rescue StandardError
    nil
  end
end

Now you'll get your valid JWT in the response body.

Bonus

I suggest you to have a look at the restful-json-api-client package which can handle JWT for you as it look for the token attribute in the responses and store and pass it to your requests.

It also handle JWT renewal automatically! So when you API replies with a 401 error, if you configure the renewal path, it will tries to renew the JWT. If it succeeded, it will redo the query with the new JWT and it's transparent for your app, otherwise if it fails to renew it, it returns the failure to your app so that you can do something with it like redirecting your user to a login page.

Hopefully this time I'm answering a little bit more to your question.