waiting-for-dev / devise-jwt

JWT token authentication with devise and rails
MIT License
1.25k stars 129 forks source link

Storing the JWT token in a httpOnly cookie #126

Closed njt1982 closed 5 years ago

njt1982 commented 5 years ago

So I get that on sign up / sign in, the token it passed back in the response in the authorisation header... However it seems to me this is putting responsibility of storing the JWT securely.

If I were building a React app, for example, almost all tutorials I've seen suggest I should trust a cookie with my JWT. LocalStorage is not recommended as a secure location to store it as its accessible under and XSS attack. Ideally, a httpOnly cookie set by the server and just left to the Browser to deal with would be the most secure, wouldn't it?

Is it possible to configure Devise JWT in this way?

It feels like the most secure approach for an app would be to never have to deal with the token at all; just let the tried-and-trusted tech (browser cookies) handle storage and transfer?

Am I missing something?

waiting-for-dev commented 5 years ago

The problem (which is not a problem, but a security measure) with cookies is that they can't be shared cross-domain. If your client and server live in different domains or servers, then you are stuck with headers, params and request/response body for client/server communication. If they live in the same domain, then you can use cookies for everything, including authentication, so you don't need a token based authentication at all.

It goes without saying that even you have to send the token to the server through the headers request, the client can store it in a cookie instead of the local storage.

njt1982 commented 5 years ago

Thanks @waiting-for-dev!

That makes perfect sense. So I’d have to have my rails app provider the authorisation header in the sign in response and my react app would have to extract that and store it in the cookie for client side persistence? I guess that solves the CSRF issue as you have to manually sign all your API requests with an auth header...

waiting-for-dev commented 5 years ago

So I’d have to have my rails app provider the authorisation header in the sign in response and my react app would have to extract that and store it in the cookie for client side persistence?

That's it.

I guess that solves the CSRF issue as you have to manually sign all your API requests with an auth header...

The server will only accept tokens from which the signer is itself, so that prevents from CSRF attacks.

waiting-for-dev commented 5 years ago

I guess this can be closed but feel free to add more comments if needed.

GMolini commented 5 years ago

What do you think of the approach suggested here? https://pragmaticstudio.com/tutorials/rails-session-cookies-for-api-authentication

@njt1982 What did you end up doing?

njt1982 commented 5 years ago

Hi @GMolini - tbh the project that needed this got put on a back burner shortly after asking this (I plan on coming back to it!). Currently I've gone with Local Storage as its easy to implement but with a massive "TODO" at the top of the file to switch it out ;)

I do something like this...

  authenticate(email, password) {
    const data = {
      user: {email: email, password: password}
    };
    localStorage.removeItem('JWT_KEY_NAME');

    return axios.post(this.apiHost + 'users/sign_in', data)
      .then(function (response) {
        localStorage.setItem('JWT_KEY_NAME', response.headers.authorization);
        return response.data
      })
      .catch(function (error) {
        console.error(error);
        throw(error)
      });
},

Then on request I do:

  authHeader() {
    return {
      headers: {
        Authorization: `${localStorage.getItem('JWT_KEY_NAME')}`
      }
    }
  },

  authRequest(method, path, params = {}) {
    const config = {
      method,
      url: this.apiHost + path,
      ...this.authHeader(),
      ...params
    };
    return axios(config)
      .catch(function (error) {
        if (error.response.status === 401) {
          localStorage.removeItem('JWT_KEY_NAME');
          window.location.href = '/login';
        }
        console.error(error);
        throw(error)
      });
},
GMolini commented 5 years ago

thanks for taking the time to respond! Ill have a look at it

kvsm commented 5 years ago

The problem (which is not a problem, but a security measure) with cookies is that they can't be shared cross-domain. If your client and server live in different domains or servers, then you are stuck with headers, params and request/response body for client/server communication. If they live in the same domain, then you can use cookies for everything, including authentication, so you don't need a token based authentication at all.

This isn't really true, you absolutely can send cookies cross-domain, you only have to configure CORS correctly (and securely). I post here because I'm looking to do this very thing - I want devise to return my token in an httpOnly cookie, and then read it from the cookie on subsequent api requests. Does devise-jwt support this?

waiting-for-dev commented 5 years ago

There is no reliable way of sharing cokies between domains. If user doesn't allow third party cookies in his browser, then your CORS configuration won't work. So devise-jwt doesn't support it, but you can do it manually taking the generated token from the headers and putting it into a cookie through a rack middleware.

kvsm commented 5 years ago

Fair point! Thanks for pointing me in the right direction.

veeral-patel commented 4 years ago

@njt1982 Did you end up storing the JWT in a cookie? If you could maybe share your sample code, if you have it, I'd really appreciate it!

njt1982 commented 4 years ago

@veeral-patel sorry, no - the code still uses local storage as an "it works, lets fix it later" step... which never got fixed. (It's a personal and unpublished project atm).

veeral-patel commented 4 years ago

@njt1982 No worries. Do you have any suggestions for me for authentication (using Rails API + React SPA) if you shouldn't store JWTs in cookies, due to cross-domain issues, and if localStorage is insecure?

moa-novae commented 4 years ago

I just started reading about this recently, so correct me if I am wrong. Wouldn't storing the JWT token in a cookie and signing API requests with an Authorization header be susceptible to xss? since they can read contents of a non-httponly cookie. So essentially, it would be the same as storing in localstorage

theblang commented 3 years ago

An additional reason we are looking to store the token in a cookie is to make use of the Domain attribute to share auth state across subdomains. From the MDN docs:

Domain attribute

The Domain attribute specifies which hosts are allowed to receive the cookie. If unspecified, it defaults to the same origin that set the cookie, excluding subdomains. If Domain is specified, then subdomains are always included. Therefore, specifying Domain is less restrictive than omitting it. However, it can be helpful when subdomains need to share information about a user.

For example, if Domain=mozilla.org is set, then cookies are available on subdomains like developer.mozilla.org.

jeremylynch commented 2 years ago

Please see this fork of devise-jwt: https://github.com/scarhand/devise-jwt-cookie

It is not clear if this gem support cross-domain JWT cookie. https://github.com/scarhand/devise-jwt-cookie/issues/11

absolute-boi commented 1 year ago

@jeremylynch to answer your question, yes, the devise-jwt-cookie supports http only cookie authentication with devise. You need to use the rack-cors gem to configure a cors config file to allow headers across domains. as stated above, this is a way to get cookie based authentication w/o the xss threat. however, also stated above, this will not work if a user specifically sets their browser to block cross domain cookies. so keep that in mind.