4

I have been struggling with this for a few hours now on how to solve this issue and I just can't seem to be able to get around it for now.

Am building a simple authentication system with a Vue front-end(created using vue-cli) and Laravel 5.8(api) for the backend; to test out the idea of using httponly cookie for authentication and protection of access to certain routes after reading this article. I am using tymondesigns/jwt-auth for authentication instead of laravel passport as used in the article and am also using barryvdh/laravel-cors package to add CORS (Cross-Origin Resource Sharing) headers support.

BACKEND

Here is my code in the routes/api.php

Route::group(['prefix' => 'auth', 'namespace' => 'Auth'], function () {
    Route::post('login', 'AuthController@login');

    Route::group(['middleware' => ['auth.api'],], function () {
        Route::get('me', 'AuthController@me');
        Route::post('logout', 'AuthController@logout');
    });
});

And the code for the middleware that am using is as follows in the app/Http/Kernel.php

'auth.api' => [
    \App\Http\Middleware\AddAuthTokenHeader::class,
    'throttle:60,1',
    'bindings',
    'auth:api',
    \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
],

And here is my code in the app/Http/Controllers/Auth/AuthController.php

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Requests\Auth\LoginRequest;

use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    /**
     * Authenticate user via given credentials.
     *
     * @param \App\Http\Requests\Auth\LoginRequest $request
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(LoginRequest $request)
    {
        $credentials = $request->all(['email', 'password']);

        if (!$token = auth()->attempt($credentials)) {
            return response()->json(['error' => 'Invalid credentials'], 401);
        }

        $cookie = $this->getCookie($token);

        return response()->json([
            'token' => $token,
            'user' => auth()->user(),
        ])->withCookie($cookie);
    }

    /**
     * Set cookie details and return cookie
     *
     * @param string $token JWT
     *
     * @return \Illuminate\Cookie\CookieJar|\Symfony\Component\HttpFoundation\Cookie
     */
    private function getCookie($token)
    {
        return cookie(
            env('AUTH_COOKIE_NAME'),
            $token,
            auth()->factory()->getTTL(),
            null,
            null,
            env('APP_DEBUG') ? false : true,
            true,
            false,
            'Strict'
        );
    }

    public function logout()
    {
        // ...
    }

    public function me()
    {
        // ...
    }
}

And the code for the handle method in the middleware class(app/Http/Middleware/AddAuthTokenHeader.php) used in the custom middleware auth.api is

public function handle($request, Closure $next)
{
    $cookie_name = env('AUTH_COOKIE_NAME');

    if (!$request->bearerToken()) {
        if ($request->hasCookie($cookie_name)) {
            $token = $request->cookie($cookie_name);

            $request->headers->add([
                'Authorization' => 'Bearer ' . $token
            ]);
        }
    }

    return $next($request);
}

As you can see in my AuthController once the login request is successfully the json response is sent along with the http-only cookie.

NOTE: Am using php artisan serve to run my backend

FRONT-END

After running npm run serve in my vue-cli generated project, I go to the login route which displays the Login component represented by @/views/Login.vue.

Here is my code for the Login.vue

<template>
    <div>
        <form @submit.prevent="submit" autocomplete="off">
            <p>
                <label for="email">Email: </label>
                <input
                    type="email"
                    name="email"
                    id="email"
                    v-model.lazy="form.email"
                    required
                    autofocus
                />
            </p>
            <p>
                <label for="password">Password: </label>
                <input
                    type="password"
                    name="password"
                    id="password"
                    v-model.lazy="form.password"
                    required
                />
            </p>
            <button type="submit">Login</button>
        </form>
    </div>
</template>

<script>
import axios from 'axios';

export default {
    name: 'login',
    data() {
        return {
            form: {
                email: '',
                password: '',
            },
        };
    },

    methods: {
        async submit() {
            const url = 'http://localhost:8000/api/auth/login';
            const response = await axios.post(url, this.form, {
                headers: {
                    Accept: 'application/json',
                    'Content-Type': 'application/json',
                },
            });

            console.log(response);
            // this.$router.replace({ name: 'home' });
        },
    },
};
</script>

Given valid credentials on route(http:localhost:8080/login), the cookie will be returned as seen in the response headers below

enter image description here

but for some reason, it's not being set in the browser cookie cookie storage as shown below

enter image description here

NOTE: The cookie shown above is from me testing if everything is running fine after running php artisan serve and opening http:localhost:8000 in the browser.

Question is, why isn't the cookie being stored in the browser cookie storage.

I should note that when I call the same backend api routes using POSTMAN, everything works fine(with the cookie being set on login and cleared on logout).

Thank you.

kellymandem
  • 1,709
  • 3
  • 17
  • 27
  • 2
    I'm not familiar with PHP so cannot give a complete answer but my first thought is that you're missing `withCredentials` on the request. See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials. By default this is not enabled with axios but you can enable it with `withCredentials: true`. I believe you'll also need to ensure that your server is not using `*` for `Access-Control-Allow-Origin`, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin – skirtle Nov 06 '19 at 06:31
  • @skirtle Thank you for the tips. Let me try out your recommendations and see how that goes. – kellymandem Nov 06 '19 at 06:56
  • @skirtle I have tried setting `withCredentials: true` in my axios headers and limiting `Access-Control-Allow-Origin` to calling client side but it's still not working. – kellymandem Nov 06 '19 at 10:04
  • 2
    `withCredentials` is not a header. Please check the axios documentation and ensure you are putting it in the correct place. – skirtle Nov 06 '19 at 10:23
  • @skirtle I managed to solve it thanks to you. Please check out my answer below. – kellymandem Nov 06 '19 at 11:30
  • @kellymandem I am still learning about this approach (storing jwt in cookie) and your question and answer has helped me a lot. I just wonder why you still send a token back to the front end? Also, I guess this method drops the need to have refresh tokens? – digout Feb 09 '20 at 12:11
  • @digout I have not used refresh tokens technique before, so I can't speak with much authority on that but as to the issue of sending the token back to the frontend, I think the reason for that is if you look at the code in the file `app/Http/Middleware/AddAuthTokenHeader.php` you do notice that before any API routes are reached the authorization header is added with the value coming from the cookie in the browser/frontend. – kellymandem Feb 10 '20 at 08:59
  • @kellymandem thanks yeah I noticed that, but both in your finished sample and the tutorial sends the token back in the return response also. Just wondered why. – digout Feb 10 '20 at 10:33
  • 1
    @digout I see where you are coming from; I think you are referring to this code `return response()->json(['token' => $token, 'user' => auth()->user(),])->withCookie($cookie);`. In my case I did that so I could possibly use it in my `vuex` code for user authentication but as it turns out, I did not need it. – kellymandem Feb 10 '20 at 11:55
  • It is necessary to set `withCredentials: true` when using XMLHTTPRequest `credentials: true` when using Fetch in the request that expects to receive credentials. This appears to only be the case when the cookie returned sets `httpOnly: true`. I'm making this note, as I faced a similar issue and ended up on this thread. My issue was that the received cookie was not being stored by Firefox 75.0 nor Google Chrome 81.0.4044.113. I had assumed that the browser would automatically store the cookie and that credentials flag would denote whether the cookie should be used in subsequent requests. – errolflynn May 08 '20 at 20:11
  • @errolflynn From my experience so far, it's the only way. – kellymandem May 09 '20 at 14:45
  • @kellymandem I noted this because it was difficult for me to find that information in the documentation – errolflynn May 11 '20 at 03:59

2 Answers2

8

All credit to @skirtle for helping me to solve this; your help was invaluable.

Here are the steps I took to make it work.

BACKEND

I run the command php artisan vendor:publish --provider="Barryvdh\Cors\ServiceProvider" which creates a file cors.php under the config directory.

I then changed this line 'supportsCredentials' => false, in config/cors.php to 'supportsCredentials' => true, and I left everything else the same after which I run the following commands just to make sure the new changes were captured.

php artisan config:clear
php artisan cache:clear

php artisan serve

FRONTEND

All I had is to change my submit method in the Login.vue to this

async submit() {
    const url = 'http://localhost:8000/api/auth/login';
    const response = await axios.post(url, this.form, {
        withCredentials: true,
        headers: {
            Accept: 'application/json',
            'Content-Type': 'application/json',
        },
    });

    console.log(response.data);
    // this.$router.replace({ name: 'home' });
},

And now the cookie is being set as shown below

enter image description here

Once again, all thanks and credit to @skirtle the tips.

kellymandem
  • 1,709
  • 3
  • 17
  • 27
0

You can setting httponly=false for accessing to cookie on front-end.

Laravel provides an option for this:

cookie($name, $value, $minutes = 0, $path = null, $domain = null, $secure = false, $httpOnly = false)
soroush
  • 170
  • 2
  • 9