The code to set session cookie is following:
res.cookie(newCookieName, sessionCookie, {
domain: getCookiesDomain(),
maxAge: ms('30 days'),
secure: true,
httpOnly: true,
sameSite: 'lax'
});
On logout we do this:
function clearOneSessionCookie(res:Response, cookieName, sameSite?: 'lax'|'strict'|'none'):void {
if (sameSite) {
res.clearCookie(cookieName, {
domain: getCookiesDomain(),
maxAge: -1000,
httpOnly: true,
secure: true,
sameSite: sameSite
});
} else {
res.clearCookie(cookieName, {
domain: getCookiesDomain(),
maxAge: -1000,
httpOnly: true,
secure: true
});
}
}
clearOneSessionCookie(res, newCookieName);
clearOneSessionCookie(res, newCookieName, 'lax');
clearOneSessionCookie(res, newCookieName,'strict');
clearOneSessionCookie(res, legacyCookieName);
clearOneSessionCookie(res, legacyCookieName, 'lax');
clearOneSessionCookie(res, legacyCookieName,'strict');
we apply all possible options of clearOneSessionCookie because at various stages of our project moving to different cookie name and options, we used different sameSite options.
I even updated to latest express, cookie-parser packages in hope for fixing that, but no effect so far.
After logout requests, following information displayed at Cookies tab at Google Chrome:
Request Cookies:
old_cookie_name: domain = www.example.com, path = /, expires = Future_Date_1, HttpOnly = yes, Secure = yes, SameSite=[not set!]
new_cookie_name: domain = www.example.com, path = /, expires = Future_Date_2, HttpOnly = yes, Secure = yes, SameSite=Lax
Response Cookies:
old_cookie_name: domain = www.example.com, path = /, max-age: -1000 ms, HttpOnly = yes, Secure = yes, Same-Site: Lax
old_cookie_name: domain = www.example.com, path = /, max-age: -1000 ms, HttpOnly = yes, Secure = yes, Same-Site: Strict
new_cookie_name: domain = www.example.com, path = /, max-age: -1000 ms, HttpOnly = yes, Secure = yes, Same-Site: Lax
new_cookie_name: domain = www.example.com, path = /, max-age: -1000 ms, HttpOnly = yes, Secure = yes, Same-Site: Strict
Notice that there is now row like
old_cookie_name: domain = www.example.com, path = /, max-age: -1000 ms, HttpOnly = yes, Secure = yes, Same-Site: [not set]
So, it seems that call
clearOneSessionCookie(res, legacyCookieName);
Does not work or not understood/recognized by browser.
This specific set-cookie has been ignored:
set-cookie: old_cookie_name=; Max-Age=-1; Domain=www.example.com; Path=/; Expires=Sat, 04 Jun 2022 15:27:10 GMT; HttpOnly; Secure
What is the best way to clear a cookie with old name and without explicit Same-Site value applied?
Thanks for your answers and time.
Update:
Clarification of symptoms of problem:
The legacyCookieName cookie with not set (no value for SameSite attribute) as outcome the existing users who was signed in before migration to new cookie setting are unable to sign out of website.
Update
Even If I made a workaround for this problem (see marked answer), if someone would offer a better working solution while bounty is active, prize is yours.
After trying various tricks and tweaks to settings of legacy cookie removal API call, I came to conclusion that so far there is a single workaround for the bug being described:
add to responses a new cookie like authProtocolVersion, with value like 1 if session involved in request processing.
If this cookie was already present at stage of request arrival and request must interact with session, this will update the session cookie analysis behavior to skip/ignore legacySyssionCookie even if it still present, and newCookieName not present on request arrival.
still sending on logout cookie cleanup during logout the clearOneSessionCookie calls in all possible combinations of SameSite - not set, none, lax, strict, to make sure that eventually the legacy auth cookie will be cleared as soon as that bug with cookies cleanup will be fixed at browser side.
with the new chrome update
if you don't specify sameSite then it defaults to sameSite:'lax' by default.
so if you set a cookie with sameSite:'lax' and clearCookie with no SameSite property then the cookie gets deleted. because no samesite is = sameSite:'lax'
Related
I host my application on vercel.com. At first the cookie is not working at all, I tried to add the options in the cookie by many solutions like
{
path: '/',
secure: true,
sameSite: 'none'
}
and it works but unfortunately, it appeared as the cookie is not shown in the browser, but on the api the cookie is set which made me confused. So I thought maybe I forgot to add domain option when setting cookie, then I added domain in the cookie option like:
{
domain: 'mydomain.me'
path: '/',
secure: true,
sameSite: 'none'
}
Then the cookie is not working again and I when I checked in Network it said, this attempt to set a cookie via set-cookie header was blocked because its domain attribute was invalid with regards to the current host url.
What really went wrong here, why the cookie is not set when I add domain option?
And when I did not add domain why the cookie is set but not showing on the browser?
Context:
In our Node.js & express based app I updated the cookie settings from
res.cookie(newCookieName, sessionCookie, {
domain: getCookiesDomain(),
maxAge: ms('30 days'),
secure: true,
httpOnly: true
});
To this slightly different options by adding explicitly sameSite property:
res.cookie(newCookieName, sessionCookie, {
domain: getCookiesDomain(),
maxAge: ms('30 days'),
secure: true,
httpOnly: true,
sameSite: 'lax'
});
And as result, I see 2 values of cookies stored at browser, probably due different sameSite values ("default" and explicitly set lax value).
Browser: Chrome 102, OS: Windows 11.
Who else experienced this double-cookies at case of updated cookie settings?
Update
Possible cause is that at least Chrome (or may be other browsers as well?) threat different configuration of these flags and options like HttpOnly, Secure or SameSite as part of cookie full identity, among with origin and name of cookie.
It is possible to mitigate this problem by these tactics:
if you use some different format in new version of cookie, like s:value.signature in old cookie and prefix.JWT_base64url you will have to scan all arriving cookie values by parsing manually arriving Cookie header to be able detect this case. Standard Express cookie parsing code for express-session useless in this tricky scenario.
The second part is to clear manually cookies with deprecated/legacy options like Secure, HttpOnly and absent or different SameSite values. So if you did not specify SameSite, than later used Lax value and finally settled on Strict value, you will have 3 separate cookies, so you will need 2 cookie cleanup commands (2 additional set-cookie headers) set with response to clear these old versions of cookies.
Of course you can add custom properties to Request in express to mark request with flags mentioning whether such extra headers should be sent with response for request where multi-cookie issue was detected to send extra cleanup set-cookie headers with empty value and negative maxAge in res.cookie cleanup call only when such custom flags present on request, if you use this tweak.
Note that standard res.clearCookie provided by Express unaware of this issue and you can not provide the related options there, as I know.
I want to expire session when the browser is closed.
I'm using res.cookie('testlocale', 'en', { maxAge: 900000, httpOnly: true });
Also which event should i use in front end(onbeforeunload or onunlod)
I'm not able to understand how to do this. I'm using handlebars in front-end
If you use express-session.
You can set expires: false.
req.session.cookie
Each session has a unique cookie object accompany it. This allows you to alter the session cookie per visitor. For example we can set req.session.cookie.expires to false to enable the cookie to remain for only the duration of the user-agent.
From docs
If you want to manually delete cookies on frontend, I think both are good fit. But I'm not sure it's good idea.
I have:
1) A client side app that has its own domain: http://client.com
2) A server side app that has a separate domain: http://server.com
Now,
the scenario is:
1) Opening http://client.com/home in the browser, which displays an HTML page.
2) http://client.com/home redirects to http://server.com/login
3) http://server.com/login stores a cookie 'auth' and sends a redirect instruction to http://client.com/welcome
Response:
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 104
Content-Type: text/html; charset=utf-8
Date: Wed, 16 Jan 2019 10:47:11 GMT
Location: http://client.com/welcome
Set-Cookie: auth=1479da80-197c-11e9-ba74-59606594e2fb; Path=/
Vary: Accept
X-Powered-By: Express
4) The browser receives the response, which does contain the cookie 'auth'
5) The browser redirects itself to http://client.com/welcome
6) 'auth' cookie is sent to http://client.com/welcome
Request:
Cookie: auth=1479da80-197c-11e9-ba74-59606594e2fb
7) http://client.com/welcome returns HTML but does not return the cookie 'auth'
8) http://client.com/welcome makes an AJAX request to http://server.com/data (CORS enabled), but the cookie 'auth' is not sent
9) http://server.com/data doesn't recognize the user because there is no cookie
The client side is an angular app hosted by Node.js
Edit:
As suggested, I've added to the response of server.com:
Access-Control-Allow-Credentials: true
but nothing has been changed.
Relevant client side code:
const headerOptions = new HttpHeaders({
'Content-Type': 'application/json', 'withCredentials': 'true', 'Access-Control-Allow-Origin': 'true', 'Access-Control-Allow-Credentials': 'true'
});
this.httpClient.get<any>(this.baseUrl + "data", { headers: headerOptions }).subscribe((res) => {
You should use the withCredentials option when sending your ajax request to your http://server.com and your server.com should have the Access-Control-Allow-Credentials set to true.
Example code in Node.JS server:
var cors = require('cors');
var corsOptions = {
origin: '*',
credentials: true };
app.use(cors(corsOptions));
More on this here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
Example code in Angular.JS client
import {RequestOptions, Request, RequestMethod} from '#angular/http';
const options = new RequestOptions({
method: RequestMethod.Post,
url: 'https://google.com',
withCredentials: true
});
More on this here: https://angular.io/api/http/RequestOptions
Also check this out: https://github.com/angular/angular/issues/24283 - it looks like a particular version of Angular had problems with this flag, so unless you're using a more up-to-date version, you might need to set the header explicitly.
The reasoning of this is, unless the server explicitly tells the client "I will accept cookies (set previously on my domain) passed by another domain" - accepting the cookies would be a security issue. More here: https://en.wikipedia.org/wiki/Cross-site_request_forgery
Your description of what is happening does not seem right.
http://server.com/login stores a cookie 'auth' and sends a redirect instruction to http://client.com/welcome
'auth' cookie is sent to http://client.com/welcome
That is not (or at least should not be) what is happening. When the browser requests http://server.com/login and gets back in the response a Set-Cookie header, the cookie is set on and restricted to the server.com domain, even if the response is a redirect. If you are seeing the 'auth' cookie sent to client.com then that is a cookie that client.com previously set.
Anyway, it seems that what you really care about is
http://client.com/welcome makes an AJAX request to http://server.com/data (CORS enabled), but the cookie 'auth' is not sent
There are a bunch of reasons this can happen.
CORS. You mentioned it was CORS enabled, but for the sake of others reading this, you must have the following CORS headers set on server.com
Access-Control-Allow-Origin: http://client.com
Access-Control-Allow-Credentials: true
Note that you cannot get away with using a wildcard for Access-Control-Allow-Origin when you are sending credentials. Also note that the origin has to be an exact match, including scheme (http or https). In practice, what servers generally do is read the Origin header of the request, check it against a white list, and if it allowed, copy the Origin header value from the request to the Access-Control-Allow-Origin header in the response.
You must set xhr.withCredentials = true in your XHR request. (See MDN for more details.)
Then after you have done all that, you have one other hurdle in your way. Because you are on client.com and trying to send a cookie to server.com, the server.com cookie is considered a "third-party" cookie. AFAIK all the major browsers have a setting that blocks third-party cookies for privacy, because they are most often used by trackers to gather marketing data for advertising. I believe most of them block third-party cookies by default, but I am not sure of that. For sure lots of people have set their browsers to block third-party cookies.
So you have to tell your visitors to configure their browser to allow third-party cookies from server.com.
BTW, it is not safe to set a cookie on a redirect to a different domain. While it is allowed under the specification AFAIK, there have been issues with browser support. See, for example, this Chrome bug.
From Access-Control-Allow-Origin spec:
For requests without credentials, the literal value "" can be specified, as a wildcard;*
Try to add specific domain to Access-Control-Allow-Origin field.
I think you should use proxy in angular angular app. For more info check this link: https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/proxy.md
I have an Express 4.x app, and I pass a cookie to the browser with
res.cookie('foo','bar1', {maxAge:99999999999});
it expires in the distant future. However, 5 minutes later, I get another request from the same user, and I want to give them a new cookie.
res.cookie('foo','bar2', {maxAge:99999999999});
From my debugging, it looks like the new cookie doesn't overwrite the old cookie? Is that the case? How can I update/overwrite the old cookie with the new one?
I am about 99% certain that using the {overwrite: true} property will do the trick - from here:
https://www.npmjs.com/package/cookies
it says:
cookies.set( name, [ value ], [ options ] ) This sets the given cookie
in the response and returns the current context to allow chaining.
If the value is omitted, an outbound header with an expired date is
used to delete the cookie.
If the options object is provided, it will be used to generate the
outbound cookie header as follows:
maxAge: a number representing the milliseconds from Date.now() for
expiry expires: a Date object indicating the cookie's expiration date
(expires at the end of session by default). ... overwrite: a boolean
indicating whether to overwrite previously set cookies of the same
name (false by default). If this is true, all cookies set during the
same request with the same name (regardless of path or domain) are
filtered out of the Set-Cookie header when setting this cookie.
So that would be, in Node.js Express parlance:
res.cookie('cdt_app_token', encoded, {maxAge: 90000000, httpOnly: true, secure: false, overwrite: true});
Note that if you are using a non SSL or non TLS connection, cookies may not work if secure is true.