Axios is not sending cookies

11,039

Since your Vue App and Laravel (API) has different HOST, it will not working.

You can re-check your server response:

Set-Cookie: refresh_token=tokenvalue; expires=Mon, 04-Nov-2019 09:13:28 GMT; Max-Age=604800; path=/v1/refresh; domain=http://app.test; httponly; samesite=none

It sets the cookie to http://app.test, not http://localhost:8080. So, there is no refresh_token cookie set in your http://localhost:8080.

The very typical solution is:

  1. You need to use subdomain, and let your cookie set to the domain=.app.test (whole domain). I mean, you need to make sure Laravel and Vue under the same domain.

  2. You don't need to get the refresh_token from cookie again in your Laravel app. First, you just need to save your refresh_token you get from API, to the either localStorage or cookie at your Vue App. Then, just send your refresh_token via forms (form-data). Finally, get your refresh_token via $request->get('refresh_token').

Here is the example, just to illustrate what i mean for the second solution.

Let's assume (typically) the http://app.test/api/login would response:

{
    "token_type": "Bearer",
    "expires_in": 31622399,
    "access_token": "xxx",
    "refresh_token": "xxx"
}
import Cookies from 'js-cookie'

async login() {
    const { data } = await axios.post('http://app.test/api/login', {
        email: '[email protected]',
        password: 'secret',
    })

    const refreshToken = data.refresh_token

    Cookies.set('refresh_token', refreshToken)
},
async refreshToken() {
    const refreshToken = Cookies.get('refresh_token')

    const response = await axios.post('http://app.test/api/refresh-token', {
        refresh_token: refreshToken,
    })
}
Share:
11,039

Related videos on Youtube

Tanmay
Author by

Tanmay

I build web services.

Updated on June 04, 2022

Comments

  • Tanmay
    Tanmay almost 2 years

    I have two apps, the server-side app which is written in Laravel and the client-side app, written in VueJS. The vue app consumes the api provided by the laravel app.

    The auth flow:

    The user attempts to log in, the server sends two tokens to the client, a) access_token and b) refresh_token upon successful login. The server also sends the refresh token in the form of an httpOnly cookie to the client so that when the access token is expired, it can be refreshed using the refresh token from the cookie.

    The problem:

    When the user logs in, in the response, the server sends the following Set-Cookie header:

    Set-Cookie: refresh_token=tokenvalue; expires=Mon, 04-Nov-2019 09:13:28 GMT; Max-Age=604800; path=/v1/refresh; domain=http://app.test; httponly; samesite=none

    This means that I expect the cookie to be sent to the server whenever there is a request to the /v1/refresh endpoint. However, the cookie is not present in the request. (I've logged $request->cookie('refresh_token') in controller but it logs null).

    This whole token refreshing mechanism is handled in a vuex action:

    export function refreshToken({commit}, payload) {
        return new Promise((resolve, reject) => {
            // axios.defaults.withCredentials = true;
    
            // here, payload() function just converts the url to:
            // "http://app.test/v1/refresh"
    
            axios.post(payload('/refresh'), {}, {
                withCredentials: true, transformRequest: [(data, headers) => {
                    delete headers.common.Authorization;
                    return data;
                }]
            }).then(response => {
                let token = response.data.access_token;
                localStorage.setItem('token', token);
                commit('refreshSuccess', token);
                resolve(token);
            }).catch(err => reject(err));
        });
    }
    

    As you can see, I've set the withCredentials config to true. I am also sending the Access-Control-Allow-Credentials: true from the server. Here is my cors middleware:

    public function handle($request, Closure $next)
        {
            $whiteList = ['http://localhost:8080'];
            if (isset($request->server()['HTTP_ORIGIN'])) {
                $origin = $request->server()['HTTP_ORIGIN'];
                if (in_array($origin, $whiteList)) {
                    header('Access-Control-Allow-Origin: ' . $request->server()['HTTP_ORIGIN']);
                    header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
                    header('Access-Control-Allow-Headers: Origin, Content-Type, Authorization');
                    header('Access-Control-Allow-Credentials: true');
                    header('Access-Control-Expose-Headers: Content-Disposition');
                }
            }
            return $next($request);
        }
    

    I don't know what have I done wrong. My PHP version is: 7.3.5. Here are the request headers of /v1/refresh endpoint:

    Accept: application/json, text/plain, */*
    Accept-Encoding: gzip, deflate
    Accept-Language: en-US,en;q=0.9,bn;q=0.8
    Connection: keep-alive
    Content-Length: 15
    Content-Type: application/x-www-form-urlencoded
    Host: app.test
    Origin: http://localhost:8080
    Referer: http://localhost:8080/products
    User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.70 Safari/537.36
    

    ...and the response headers:

    Access-Control-Allow-Credentials: true
    Access-Control-Allow-Headers: Origin, Content-Type, Authorization
    Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS
    Access-Control-Allow-Origin: http://localhost:8080
    Access-Control-Expose-Headers: Content-Disposition
    Cache-Control: no-cache, private
    Connection: keep-alive
    Content-Type: application/json
    Date: Mon, 28 Oct 2019 09:40:31 GMT
    Server: nginx/1.15.5
    Transfer-Encoding: chunked
    X-Powered-By: PHP/7.3.5
    X-RateLimit-Limit: 60
    X-RateLimit-Remaining: 59
    

    I don't know the inner-workings of browser's cookie storing mechanism, I also don't know if an httpOnly cookie can be found in the filesystem, but in despair, to know whether the browser is indeed saving the cookie, I googled and found that cookies are stored in ~/Library/Application Support/Google/Chrome/Default/Cookies file, which is an SQLite file. I opened that file and searched for my cookie 🍪, but it wasn't there either (maybe httpOnly cookies are stored somewhere else?).

    Now, my question is, how do I retrieve the cookie from the client-side app?

    • nmfzone
      nmfzone over 4 years
      What is exactly your HOST? Is it localhost:8080 or app.test?
    • Tanmay
      Tanmay over 4 years
      Thanks for the response. localhost:8080 is where my vue app is served from. app.test is where my laravel app is served from. They are two different apps/domains/origins. This is a very typical set up for a SPA that is completely isolated from the laravel app.
    • nmfzone
      nmfzone over 4 years
      So, the vue app and api has different host, right? Of course it would not work, since your laravel set the cookie to app.test, not localhost:8080. Take a look at the domain path=/v1/refresh; domain=http://app.test; httponly; samesite=none.
  • Tanmay
    Tanmay over 4 years
    Changed domain to localhost:8080, cleared cache, cookies, and everything. Tried again. Still, the cookie could not be retrieved.
  • nmfzone
    nmfzone over 4 years
    What do you mean by changed domain to localhost:8080 ? You can't set cookie from Laravel app.test to http://localhost:8080. Here is why.
  • Tanmay
    Tanmay over 4 years
    I meant I changed the value of the domain= from app.test to localhost:8080 as you've mentioned.
  • Tanmay
    Tanmay over 4 years
    I am working on your first point. But your second point where you are suggesting to send the refresh token via form, it can't be done. Because this cookie is httpOnly cookie. It can't be retrieved from $request->get()
  • nmfzone
    nmfzone over 4 years
    For the second point, that's the problem. You don't need to set refresh_token as cookie again in Laravel. You just need to save your refresh_token you get on authenticate to the localStorage or cookie in your Vue App.
  • Tanmay
    Tanmay over 4 years
    I am not setting the refresh_token again. All I am trying to do is set the token to clients cookie, and retrieve the token (which is supposed to be sent automatically once it is set).
  • Tanmay
    Tanmay over 4 years
    Do you know how can I assign my vue app to the subdomain of app.test? I am using laravel valet.
  • nmfzone
    nmfzone over 4 years
    @Tanmay No, I don't know how to setup subdomain in local machine, even with valet. I would just suggest my second solution. That's the typical use case (AFAIK). But, just do what you want.
  • Tanmay
    Tanmay over 4 years
    The second solution suggests storing the refresh token to localStorage. But under no circumstances should I store the refresh token in localStorage. That would defeat the whole purpose of a refresh token. Refresh token should be stored as httpOnly cookie so that javascript can't access it. The whole point of the refresh token is to prevent javascript from accessing it.
  • nmfzone
    nmfzone over 4 years
    I said, localStorage or cookie. It depends on what's your Vue App types. If it's SSR, you can set it to the httpOnly cookie. But, if it's SPA, of course you can't set httpOnly cookie. If you don't mean to save it to cookie or localStorage in SPA, so you can use vuex (or just use simple class to hold it), just the same as Auth0 used here
  • nmfzone
    nmfzone over 4 years
    AFAIK, if you're using cookie in your API, I believe it wouldn't work if your API got accessed via Android (or similar, like cURL).
  • nmfzone
    nmfzone over 4 years
    Btw, I was forget something. Where do you store your access_token?
  • Tanmay
    Tanmay over 4 years
    I would create a different endpoint for android, let's say v1/app-refresh where instead of grabing the token from $request->cookie() I would grab it from $request->token
  • Tanmay
    Tanmay over 4 years
    I am storing accesd_token in localStorage. It only has 1 minute life time. If it gets stolen, it would already be expired by the time attacker makes another request.
  • nmfzone
    nmfzone over 4 years
    haha, that's what i've guessed. You store access_token in localStorage, so what's the difference? ;)) Ok, just do as you please.
  • Tanmay
    Tanmay over 4 years
    I had the very same question in my mind when I was naively implementing the auth. Then one day I asked myself what exactly is the purpose of a refresh token. So here is the difference: if someone steals the access_token from the localStorage, he cannot do anything with it. But if someone steals the refresh_token, then he has 7 days, which is enough to destroy the system.
  • nmfzone
    nmfzone over 4 years
    Are you sure attackers can't do anything in 1 minute? ;)) I even can access all your API just in seconds, with simple script.
  • Tanmay
    Tanmay over 4 years
    That's the point, I can change the access token's life time to 1 second.
  • nmfzone
    nmfzone over 4 years
    Then, just change refresh_token to 1 second 🤣
  • Tanmay
    Tanmay over 4 years
    It seems to me that you keep insisting that access_token and refresh_token are same thing. They aren't. Refresh tokens cannot have short lifetime
  • nmfzone
    nmfzone over 4 years
    Yes, it just the same, from the aspect of security. Sorry, I can't find that text. Where is it? I can only find refresh tokens have the potential for a long lifetime. That's it.
  • Tanmay
    Tanmay over 4 years
    Thanks for your opinion. But I refuse to accept that people who've invented the refresh token, invented it for no reason. If both access and refresh token have the same purpose, why was refresh token inventend in the first place?
  • nmfzone
    nmfzone over 4 years
    Since this conversation is far from question you're asking, I think It's enough. I'll just give you some references 1 2. Thanks for your opinion. Just don't forget to accept the answer if it's answering your original question. Have a good day!
  • nmfzone
    nmfzone over 4 years
  • amucunguzi
    amucunguzi over 3 years
    localStorage is too unsafe for a refresh token (long-lived access; attackers can keep giving themselves access).
  • nmfzone
    nmfzone over 3 years
    @amucunguzi From the aspect of security, localStorage is always unsafe, even for access token. It's just an alternative, read here.