kellymandem
kellymandem

Reputation: 1769

Httponly cookie not being set / stored (Laravel / Vue)

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.

Upvotes: 4

Views: 11915

Answers (2)

soroush
soroush

Reputation: 177

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)

Upvotes: 0

kellymandem
kellymandem

Reputation: 1769

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.

Upvotes: 9

Related Questions