Reputation: 3159
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?
Upvotes: 3
Views: 12580
Reputation: 2903
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:
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.
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,
})
}
Upvotes: 4