How to fix SLO with passport-saml that works on first logout but not on subsequent logouts - express

I have to connect an application to my company's ADFS server. I am using passport-saml for SSO and SLO. SSO works, and SLO works on the first logout only. I am trying to make SLO work every time a user logs out.
I have been searching high and low for a solution to this problem, but it evades me. Here's the details:
I clear the cookies in the browser to start with a clean slate.
I login to my application which redirects to ADFS' login page
Enter user credentials and then ADFS redirects back to my app's homepage
I log out of my app and a request is sent to the ADFS server killing my session locally and on ADFS, I am then redirected back to my app's homepage
I log in again and this works as intended
I logout but this time I am sent to my ADFS server's logout page.
Further inspection shows that ADFS is not clearing its cookies so the ADFS session stays live.
I have used Firefox's SAML viewer plugin to watch what is happening and here are my findings:
On a successful logout:
HTTP:
GET https://myadfs.org/adfs/ls/?wa=wsignout1.0 HTTP/1.1
Host: myadfs.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://example.com/dashboard/data
Connection: keep-alive
Cookie: MSISAuth=AAEAAMVBaN7qo03wm/4jDH9e/tZ6ih6HN++2S7c7c0aXHK1RYIZ5++4Y7pf3g4v+OdRUzcJgOROfZkXx0tSEeCOfJFMluodJiSYsESiJnidVcR7Os/iHkNqIp88qGG7UZj+l8NYyvsO/7soTyQGkbMqoI0Z+0z+xXz2CZgOxsqWcjJ3FmTR32bsMR8Lra77XI2KyKycFiNYdYJ2dSKC7yBdxBRKHB7LAs4DOJKAtOt//IWspe9zPbju+x6chgP0dKToyfqX6m4EwlQnbHG4hmCImtXrEDytx1rbuLiBC7N56Y9WmGBTht5vgYvVEoA2cRqBbNYK+HoonL6+oBIJdba6+XZ2lBQsO/yJowvaHxPM8wgwLBknSt39RswaSdGjrI18CcgABAAB/eeLBPuQ9dk6ItCeTem38XttX/PQPLi52Ts+ZQGYHxs4VsO1EMe7EgMGYThPGlMCDcmS9ouXOSh6yW/LiL1jTuhc2/jhq3X0jWY+XPOSXtp81mineHeNv8SWsFjggzh5AymLtPPrPUYT6ihj9fcbJymqatsZMI5B5h0gxS2LaUUWjJyRxpMIyQXEpLSx1mxU5psQrj5/nGpOiq98uy8HE4kJp+Ey9uugSZQXhn9NwY+EqqmWxf6LDrCaeMLFDIX6mlgqu2eTLrUA9gNIJ4kSOC/5Rtw4JQVJpSeQuMom6kCHFEvZo/57BIhGkgWR8vNNCguHzZeB+as0xxfxmmb9SgAMAAMVFqaMXn0uG8+IGJIfxdIIoJ7EsLqV7so7WnFT/4OxfLzsXlO2flq0vcEbasLuLoqhGFaOuy1dkq/ft9se6Pv6rQfH7Esk/aMey/cKObBUPkcZAUFtQxXD7MSLScsiVnq3hHjrpZzEnMTToVkA9Zjv3i72Wv20tdE658+7O1olibavPPIT7Z5syoQNa1rjOAaXcPlM5hbbjXm7BiXx37ZEnvxwpY1Mf4Yocvgd9kMoApciDB2csbTf4GEic7MKeAI2G5KpwArY7g+zt4BJud+F/xnyuwVPpwPVEiNbHQnAogh5NoMDwRx+macTdkHku4AdNvruS/4L/aUHcEhPlhu3j/7r9kP1EnRso12NP1AWipsGlmpdAjoIXfK0+NBqJnDq0KwSEcvJ38OI6Z1FVkRWySi8br8pjtcytFhdh5RTkpD8FVQZ/RnGC1XE4q4IJhxMBlE1Kd8PNh3p85qpoX6r2I36a3knwK2dkm7pb0XNVwhxhC5DGpaB2iNo86CGi+BX4rICBGkNgyrOW/aWKpIhLu0bo1IDVQJw7MORdROJJk/o81E15HuC2g4r3ch+IvZOXKfAenGYM2mYrgnSRHLD0p7KsDN0vuU3IdLXAL5/D5ezr3WQFDFXPpRJyQ+qfx8kyUCe/vtvEVaNezHzOKosQsNGwSvp+lHrEGA9LLYM8RkU/Vwshgkeq2H8MoyuDRaxgOoudNGOmvwNfMp9BoOsz8OCDA5R2BB+JXzsEkSpNYebJK+VWm5wOcYnJ2j9y1OKjRU1ICRtsSPG5kLWmYUt8hHsswzrj4UAxpks+Dn2S09YzeOudC5ss5hmTM/UeVG3r3kJ9+Ad7716V9g7016u+XGhfSWty8EPxVAg0qV9wwAIk+FliWFdF1OLY1RODcsS3swqYfMrBWWdULVNl5d36ycFGucaP893o4Q/im7tx2+588lfvPbZO+DkP40MHP9Hwe++ra6kDiQx5si4M16zYIMmxa4nq6XVcr2hFlqbsLQjhIqkiFOCkt9LNRdKNZlghQkspUH44qLBq4sTHK0iD13FFmBs5rEE1CWa89oCELhea/Z9hPEtjPpC3Q52cAXBgbOJCTr6OYFYfQKbATqHdTU09/nJOafMK5ID1pf7pmBL+ZTH7Kl64lxhyO/9F84t47TctQhhFqxgsIxmv+ZVHajanNl4E0gXqJ0ULsY2h; SamlSession=aHR0cHMlM2ElMmYlMmZmcGNkcmRldi5tb2ZmaXR0Lm9yZyZGYWxzZSZDdWtyYXNTRCYmJiYmXzFkZjY4M2RhLTM4NTktNDVjNS04ODNkLTA3NmRiYTdiMjk3Yg==; MSISAuthenticated=NC8xNi8yMDE5IDExOjI2OjI4IEFN; MSISLoopDetectionCookie=MjAxOS0wNC0xNjoxMToyNjoyOFpcMQ==
Upgrade-Insecure-Requests: 1
HTTP/1.1 302 Found
Content-Length: 0
Content-Type: text/html; charset=utf-8
Location: https://example.com:443/login?SAMLRequest=lZLfa4MwEMf%2fFcl71KjxR7BCqS9C18I69rCXEjXpZJq4XCz982crY6yMwh7vuO9973N3OfChH9lWn%2fRkn8XnJMA6VblCR%2bpzQqWUOKWE4igMUlz7nGKexHUYJdSnKUXOqzDQabVCgesjpwKYRKXAcmXnlE8y7EeYxC%2bEsCBmYeamCX1DTjm7dIrbm%2fLd2hGY58mxaU0rzu6gpeysdbU5eb0%2bdQo5G61AXHtORjHNoQOm%2bCCA2YYd1k9bNtuzZilik4JRNJ3sRIucnbZ7tTdraYW5HykkPyNdhl4Bu23jsctotNWN7lGR33DNIn0s4gDCXHFRccWdac04Auh7XN5K8ObSc9cI8KyZwObeYlPku7ltVf7TbjN9GA6HMvcWeZEvFz8IuB6uUq24FEfSyjgNW47DlGY4og3F6RxjP4nbmid1kCV17v2h%2fE7%2beqDiCw%3d%3d&Signature=pT%2fSUpslARJlvOCah5VzZk4stZLIREyHmUFOO4siHUbkL5eJG4QsfYj9Pq%2bwxnOaPaevYkmiXq0rft3drTzJHspns9UbucyYQvEaSAZVmRTTyfPC3Z0EgVGSvtr0JL3nuDPsq2IfbToseuQQtJFsA%2b94D8KtaLjtUJxiMcQMHyg2yR00Ac3NGt9AsRg1X73X%2frt0XZDN9bSt4R8t%2bt2Yl2UsZsL4GHTGk7RbN3AUrYHsLtKeuN07umXqX3otVtHo%2f9tx2w2h1glYycYbFCk%2bWjox8Mej%2fiLLkpAhw9EXlhiTGrEJ2%2bcYvnQxGokOsz2vXEOoc3%2fhle27LuTPFMN9yw%3d%3d&SigAlg=http%3a%2f%2fwww.w3.org%2f2001%2f04%2fxmldsig-more%23rsa-sha256
Server: Microsoft-HTTPAPI/2.0
P3P: ADFS doesn't have P3P policy, please contact your site's admin for more details
Set-Cookie: SamlSession=; expires=Mon, 15 Apr 2019 11:26:39 GMT; path=/adfs
SamlLogout=aHR0cCUzYSUyZiUyZnJwcHNzb2Rldi5tb2ZmaXR0Lm9yZyUyZmFkZnMlMmZzZXJ2aWNlcyUyZnRydXN0Pz8/aHR0cHMlM2ElMmYlMmZmcGNkcmRldi5tb2ZmaXR0Lm9yZyZGYWxzZSZDdWtyYXNTRCYmJiYmXzFkZjY4M2RhLTM4NTktNDVjNS04ODNkLTA3NmRiYTdiMjk3Yj9fNTBhMTVmZmYtODUxNS00MzI4LWIwYTUtYTc2YjM0NzUwNTg1P3VybiUzYW9hc2lzJTNhbmFtZXMlM2F0YyUzYVNBTUwlM2EyLjAlM2FzdGF0dXMlM2FTdWNjZXNz; path=/adfs; HttpOnly; Secure
MSISAuthenticated=; expires=Mon, 15 Apr 2019 11:26:39 GMT; path=/adfs
MSISAuth=; expires=Mon, 15 Apr 2019 11:26:39 GMT; path=/adfs
ReturnUrl=aHR0cHM6Ly9ycHBzc29kZXYubW9mZml0dC5vcmc6NDQzL2FkZnMvbHMvP3dhPXdzaWdub3V0MS4w; path=/adfs; HttpOnly; Secure
MSISSignoutProtocol=U2FtbA==; expires=Tue, 16 Apr 2019 11:36:39 GMT; path=/adfs; HttpOnly; Secure
Date: Tue, 16 Apr 2019 11:26:39 GMT
SAML:
<samlp:LogoutRequest ID="_50a15fff-8515-4328-b0a5-a76b34750585"
Version="2.0"
IssueInstant="2019-04-16T11:26:39.875Z"
Destination="https://example.com/login"
Consent="urn:oasis:names:tc:SAML:2.0:consent:unspecified"
NotOnOrAfter="2019-04-16T11:31:39.875Z"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
> <Issuer xmlns="urn:oasis:names:tc:SAML:2.0:assertion">http://myadfs.org/adfs/services/trust</Issuer> <NameID xmlns="urn:oasis:names:tc:SAML:2.0:assertion">USERNAME</NameID> <samlp:SessionIndex>_1df683da-3859-45c5-883d-076dba7b297b</samlp:SessionIndex> </samlp:LogoutRequest>
On subsequent, unsuccessful logouts:
HTTP:
GET https://myadfs.org/adfs/ls/?wa=wsignout1.0 HTTP/1.1
Host: myadfs.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Referer: https://example.com/dashboard/data
Connection: keep-alive
Cookie: MSISLoopDetectionCookie=MjAxOS0wNC0xNjoxMToyODoyNlpcMQ==; SamlLogout=aHR0cCUzYSUyZiUyZnJwcHNzb2Rldi5tb2ZmaXR0Lm9yZyUyZmFkZnMlMmZzZXJ2aWNlcyUyZnRydXN0Pz8/aHR0cHMlM2ElMmYlMmZmcGNkcmRldi5tb2ZmaXR0Lm9yZyZGYWxzZSZDdWtyYXNTRCYmJiYmXzFkZjY4M2RhLTM4NTktNDVjNS04ODNkLTA3NmRiYTdiMjk3Yj9fNTBhMTVmZmYtODUxNS00MzI4LWIwYTUtYTc2YjM0NzUwNTg1P3VybiUzYW9hc2lzJTNhbmFtZXMlM2F0YyUzYVNBTUwlM2EyLjAlM2FzdGF0dXMlM2FTdWNjZXNz; ReturnUrl=aHR0cHM6Ly9ycHBzc29kZXYubW9mZml0dC5vcmc6NDQzL2FkZnMvbHMvP3dhPXdzaWdub3V0MS4w; MSISSignoutProtocol=U2FtbA==; MSISAuth=AAEAAFOnxdlEvO8Le/Gti39Bx6BFj1cEJ39/A6ogocbLbXlBnq07uT1v+MuAzZs0NqyB1Wmqx3O8oTwPancFPCEFrQbngzsvsWI/oAXmuDih8uBG9MVPfstAu/cFPXL95V2IIUjX6r3Tv08FqipxW/1CHa7QM8XvXU5a516zFsZTaxke+ITD3B+nGPsuQY+oVG47NhtoMHmCrbShjOBd9Wn6Q5FzDqbHlxD/5czDUXixYf8gg+MTNq9W+oT5J7TF6NaBb7o1QojY7c8UoJ4fQONwlMNE17TgGVomqN4N9qVPTShGSaTlM8C+er9SOWQiALfZHvH2sv8N0AIn9qpivuCzw9WlBQsO/yJowvaHxPM8wgwLBknSt39RswaSdGjrI18CcgABAAAAz9AfrV1onudL+YY+0zL4vWeCboTECwksETafeI44/o0n0DEBx8kVGELmmPqSKD216OFB+p4k0K//HTW+YnRiuFpk1dAnN+dmwirgwzohFU1A3lWq0pQcHFyui1xs1UHnzDZokvK+7r859oZP0XZ4pGGTZsjWyc2B32FgwfvpiKYKDsWALpajW9FRDnt1VnGyDSzsN3V6vQHmKIEBZn5wb3+b3DtB9hV/ZssxiE7Xf8V8l+144wE71YH4ETNbcX0VXKNlkL9x5R+EThMlzyNl2tAcGWSk+3xM3lhfTm3+8y5GEP3rtJjLQGZSPKUljPcZM/MU3EX3YRrCkYsAyhgpgAMAAKGsYkEEca74go1dVexUCjdky1zUJMng5a/ZmKCRWTYsPT2DCjR579a0Hr69s8nl36p8EgyqnyXPm/uiFp+LPp1CuCCuXe/QYFoySixCOEcJsnRbikBEAP/Bpj5UUifnqgyO7MHH1GQiXeOlw2llsPu7rdNiEqB4X6Hqhnn6xaasl+5iqvNkZSTi8DSQc/24MRT4VsAcJcO7eqxjQBluWr2cyvdr9pn4GigQ05WaXWfogo3BwPJzLUo+NngvLHfxyn1wDmUYghc+oxS+vJwTadiiSDDzrcTVTuVxw2xj6OVi8DXbyRii5+VTKolRK0qCa/4C4BCzOOGUkooktX/GecV6eNuk8xOdLsiybY9Ah5Z2WVgraDntw/w/pP/ij4v0jDLvDQjU+BIfGOpeV1jcG9VDObir5GYGfOm59DtlRpoy/kpjiDLWI8EE75DEFlhomeae0v4xBQ6XqgVd5lEcA2DTm/3Ophg31FA2M5J65yE4t7W7inIC4XjMWFOu3GCMse7ERYyFbq59vf+iSs6eyev7wXidvAekALmq6Gk2Ths2JR1TbV27E2+kgGhmvlgiShx67E9s2wrBfPKvV7+IMS9Xe1YPKpZAlfCwnkbQNonqAMQH5LsHq1K7DWrNTcon10TiOtlMbzin8FtNphcnChHYmBbDxpqrf5xwwYXbyznQnMfeDnjN7aPo909gwhfUGNltLTOZ81m6k9c3Z0C8ugvL61bbw3Ku42OZiOnoVcEYjf50bMWZQl/hUMlRp+uHVNhK41z6U2O9Ph7S4ZI4wg7z33Z+VCp+08HpMRqrX155atJYVX73mnr3+J4rKvyJvjglb9aA333MUOC7iGMDDNImibvofyhbqK3VO+zqyPYj0R4OvhnA9RlvV10MWDhn5qnVevA5Oo1MQNPGnTLtfRZXpB8oa2bZZMh62XO4a5gZ/ioNsigiDAFKbQnx0wvBTb0uqYSZpfxoA4K2o87swOYB81FTkQNBnNZG171szH89jijOuEAI7hAWdAnM2LjagGZwWpuF2yHbJqQqsGzjvnqbQ6yMTvaEbkooSelFEBeRW2Gg5rGAjj5Pvs+T0ljhVlby6FfFKJ71NDBvn/7PGIglARSZqUZcAuthlhr8pta11WnhsfnyumvLfWvOZHZZjWslKMLBpGEBe1WgcYBUBYUrUeHmCqDRy5Zc4KJXwGrY; SamlSession=aHR0cHMlM2ElMmYlMmZmcGNkcmRldi5tb2ZmaXR0Lm9yZyZGYWxzZSZDdWtyYXNTRCYmJiYmX2NlNDAwODQxLTA2ZDItNDI3Ni05MTRlLWU5N2ExYWRlZmQzZQ==; MSISAuthenticated=NC8xNi8yMDE5IDExOjI4OjI2IEFN
Upgrade-Insecure-Requests: 1
HTTP/1.1 200 OK
Cache-Control: no-cache,no-store
Pragma: no-cache
Content-Length: 8957
Content-Type: text/html; charset=utf-8
Expires: -1
Server: Microsoft-HTTPAPI/2.0
Date: Tue, 16 Apr 2019 11:28:45 GMT
SAML:
NO SAML SENT
You will see that on a successful logout ADFS sets the cookies to clear them while an unsuccessful logout does not. Also, the unsuccessful logout does not send the SAML logout request.
Lastly, when I clear the cookies in the browser, the first login/logout session will work as intended again, and all subsequent logouts will not. I can see the cookies are retained on subsequent logouts as ADFS is not getting the SAML logout request. I just don't understand how this works on the first logout but not the following logouts. I have looked in and out of passport-saml's code but can't seem to find the issue.
Any assistance would be great.
Here is my passport.js setup:
const fs = require('fs');
const passport = require('passport');
const SamlStrategy = require('passport-saml').Strategy;
require('dotenv').config();
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
passport.use(new SamlStrategy({
entryPoint: 'https://myadfs.org/adfs/ls',
issuer: 'https://example.com',
callbackUrl: process.env.NODESERVERURL + ':' + process.env.PORT + '/authenticate/adfs/postResponse',
privateCert: fs.readFileSync(__dirname + '/private/keys/fpcdr.key', 'utf-8'),
logoutUrl: 'https://myadfs.org/adfs/ls/?wa=wsignout1.0',
signatureAlgorithm: 'sha256'
},
function(profile, done) {
const username = profile.nameID.toLowerCase();
const email = profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'].toLowerCase();
const sessionIndex = profile.sessionIndex;
return done(null, {
username,
email,
sessionIndex
});
})
);
module.exports = passport;
passport callbackUrl:
module.exports.adfsAuthenticate = function(req, res) {
const email = req.user.email;
const username = req.user.username;
if (process.env.UAT === 'true') {
res.status(302).redirect(LANDING_PAGE_REDIRECT_DEV);
} else {
res.status(302).redirect(LANDING_PAGE_REDIRECT_PROD);
}
};
adfs logout:
module.exports.logout = function(req, res) {
req.logout();
req.session.destroy(function (err) {
if (!err) {
res.status(200).clearCookie('connect.sid', {path: '/'}).json({status: "Success"});
} else { alert(err); }
});
};

I have observed that there is one cookie which gets stored in browser called "MSISSignoutProtocol", if this cookie exist, the logout does not work as expected on subsequent requests.
To get it working, you might need to implement proper SAML logout which most IDP support.

Here is my solution, shared here
I found out why SLO doesn't work. I found a solution to make it work properly, im my case with Microsoft Azure (SAML-ADFS), using a database session storage. I log in from my app. Then I log out from microsoft azure ( not from my app), and it logs out nicely !
I think important to mention that there is a huge difference between the IDP logout request with all others. Most of the time, passport routes handle user device's initiated requests or redirects, That means the device's cookies comes along theses requests, which allows passport-saml to successfully retreive device's user information.
When the IDP broadcasts a logout request to relevant ISP(s), it is not always a device request (either initiated nor redirected). It is a simple HTTP GET request that contains a SAMLRequest. This SAMLRequest contains what the IDP uses to identify a saml user.
A saml user is identified using nameID and nameIDFormat profile properties.
Therefore, when a user logs in through an IDP, the app must store both values, nameID and nameIDFormat. You can acheive that first in you saml strategy authentication handler :
export const samlStrategy = new SamlStrategy({
...samlStrategyOptionsHere,
logoutCallbackUrl: 'https://your.isp.com/saml/logout'
},
(profile: Profile | null | undefined, done: VerifiedCallback) => {
// do login process. On successful login, don't forget
// to pass along nameID and nameIDFormat profile values !
done(null, {
userId: user.id, // in my case, my app just need to store the user id
nameID: profile?.nameID,
nameIDFormat: profile?.nameIDFormat
})
});
Then store nameID and nameIDFormat in passport user's session ( within serialization process ) :
interface SessionUserData{
userId: number,
nameID?: string,
nameIDFormat?: string,
}
passport.serializeUser<SessionUserData>((user: any, done) => {
// do serialization stuff
done(null, {
userId: user.id,
nameID: user?.nameID,
nameIDFormat: user?.nameIDFormat
})
});
SLO requests can then be handled properly by first retreiving passport sessions that holds the specified saml identification, and destroy them manually, like this :
interface SAMLLogoutExpectedResponse{
profile?: Profile | null | undefined;
loggedOut?: boolean | undefined;
}
authRouter.get('/saml/logout', async (req, res)=>{
// req.user may not exists here, since this is a single HTTP-GET request that contains a SAMLRequest
// initiated by the IDP, not a user device redirection
// use passport-saml to validate this request
const response:SAMLLogoutExpectedResponse = await (samlStrategy._saml as SAML).validateRedirectAsync(req.query, url.parse(req.url).query)
// check the « SAML USER » profile
if( response?.profile &&
response.profile?.nameID &&
response.profile?.nameIDFormat &&
response.loggedOut===true ){
// get related session(s) if exists
const sessionsToDestroy:string[] = ((await knex.from('sessions').where(true).select('*')) ?? []).reduce((acc:any[], session:any) => {
const userData:any = JSON.parse(session?.data ?? {})?.passport?.user
if( userData &&
userData.nameID &&
userData.nameIDFormat &&
userData.nameID == response?.profile?.nameID &&
userData.nameIDFormat == response?.profile?.nameIDFormat
){
acc.push(session.session_id)
}
return acc;
}, [])
if(sessionsToDestroy.length){
// destroy database session --> which invalidate any corresponding cookie on any device (if still exists)
await knex.from('sessions').whereIn('session_id', sessionsToDestroy).delete();
}
}
// if this is a IDP request, all ends here
// if user initiated the logout request, IDP will redirect device here. Send the device somewhere coherent after successful logout
res.redirect('/')
});
Voilà. I know this is a precise case, and doesn't fit to everyone. But the most important point is that a SLO callback must handle nameID and nameIDFormat values. It should not handle a device session.

Related

Nextjs pass cookie from external API and set on calling domain

I have a Nextjs app running at domain like https://client.mydomain.com that makes calls, from browser not getServerSideProps or micro api, to Express API at https://api.mydomain.com. The API returns a cookie (Secure, SameSite='none', long Expires), I can see the cookie in the response, and subsequent calls to the API from the browser will include the cookie correctly, but the cookie is not ever set on https://client.mydomain.com. So if I return to https://client.mydomain.com, the cookie is not present. Like it somehow pertains to the shared singleton axios instance.
How do I allow the cookie to pass through correctly from the API domain > client domain? > browser to be set on https://client.mydomain.com? I can extract the cookie from API in the axios call response in the client browser and manually set it via document.cookie of course, but sure I can simply allow it to pass through somehow from the API?
Express
https://api.mydomain.com
...
app.use(
cors({
origin: ['https://client.mydomain.com', 'https://localhost:80'],
credentials: true,
exposedHeaders: 'Set-Cookie',
}),
);
...
res.cookie('mycookie', 'hello', {
sameSite: 'none',
secure: true,
expires: tokenExpires,
});
...
Next.js App
https://client.mydomain.com
const response = await axios.get('/', {
withCredentials: true,
});
Response Header
set-cookie: mycookie=hello; Path=/; Expires=Fri, 29 Apr 2022 21:43:00 GMT; Secure; SameSite=None
Yet no cookie persisted if I check the application tab in chrome, or run document.cookie after the requests to API

Safari doesn't send cookie to Express when requesting image via p5 loadImage()

Background
I set up an Express.js app behind a proxy to let users to login before being directed to a web app. This app is failing to serve up some images in Safari (macOS/iOS) because Safari is not sending the cookie back with requests for images that originate from the loadImage() method in my p5.js code. This does not happen on Chrome (macOS).
When I load the page, the browser requests the resources fine. But requests originating from my application returns a different session, which is not logged in, and gets caught by Express:
// Request for the main page by the browser
Session {
cookie:
{ path: '/',
_expires: 2020-05-04T16:26:00.291Z,
originalMaxAge: 259199998,
httpOnly: true,
secure: true },
loggedin: true }
// Request for image assets by a script in my application
Session {
cookie:
{ path: '/',
_expires: 2020-05-04T16:26:00.618Z,
originalMaxAge: 259200000,
httpOnly: true,
secure: true } }
HTTP Requests from Safari
GET https://mydomain/app/img/svg/Water.svg HTTP/1.1
Host: mydomain
Origin: https://mydomain
Connection: keep-alive
If-None-Match: W/"5e6-171c689d240"
Accept: image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5
If-Modified-Since: Wed, 29 Apr 2020 15:24:13 GMT
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15
Referer: https://mydomain/app/
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br
HTTP/1.1 302 Found
Date: Fri, 01 May 2020 06:50:07 GMT
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.3.17
X-Powered-By: Express
Location: /
Vary: Accept
Content-Type: text/plain; charset=utf-8
Content-Length: 23
Access-Control-Allow-Origin: *
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Found. Redirecting to /
Express app
The app is set up behind an HTTPS proxy, so I set the Express Session object to trust proxy and set security to auto (setting to false doesn't fix the problem):
app.set('trust proxy', 1)
app.use(session({
secret: 'my-secret',
resave: true,
saveUninitialized: true,
cookie: {
secure: 'auto',
maxAge: 259200000
}
}));
When the user signs in, it is sent to /auth to check against the database
app.post('/auth', function (request, response) {
var user = request.body.user;
var password = request.body.password;
if (user && password) {
connection.query('SELECT * FROM accounts WHERE user = ? AND password = ?', [user, password], function (error, results, fields) {
if (results.length > 0) {
request.session.loggedin = true;
// If the user logs in successfully, then register the subdirectory
app.use("/app", express.static(__dirname + '/private/'));
// Then redirect
response.redirect('/app');
} else {
response.send('Incorrect password!');
}
response.end();
if (error) {
console.log(error);
}
});
} else {
response.send('Please enter Username and Password!');
response.end();
}
});
They are redirected to /app when logged in:
app.all('/app/*', function (request, response, next) {
if (request.session.loggedin) {
next(); // allow the next route to run
} else {
// The request for images from my p5 script fails and these request redirect to "/"
response.redirect("/");
}
})
Question
What can I do to ensure Safari pass the session cookie with its request so that Express will return the correct asset?
Edit
Including the function that invokes loadImage(). This is embedded in an ES6 class that loads image assets for particles in a chemical simulation. This class must successfully resolve promises so other higher order classes can set correct properties.
loadParticleImage() {
return new Promise((resolve, reject) => {
loadImage(this.imageUrl, (result) => {
// Resolves the Promise with the result
resolve(result);
}, (reason) => {
console.log(reason);
});
})
}
Edit #2
Including the headers for a successful request directly to the URL of the image asset:
GET https://mydomain/app/img/svg/Water.svg HTTP/1.1
Host: mydomain
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Cookie: connect.sid=s%3A2frOCv41gcVRf1J4t5LlTcWkdZTdc8NT.8pD5eEHo6JBCHcpgqOgszKraD7AakvPsMK7w2bIHlr4
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1 Safari/605.1.15
Accept-Language: en-us
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
I suggest using express's static middleware to serve static files. With this, you won't need any session to get images, js, css, etc. Also, it accelerates your application. You need to place
app.use(express.static( ... ))
before the app.use(session( ... )) statement if you want some additional perfomance, because if you do, express won't attepmt to creare session for static files.
The fetch() call in the source code for that loadImage() function is not setting the credentials option that controls whether cookies are included with the request or not, therefore they are not sent with the request.
Do you really need authentication before serving an image? If not, you could rearrange the way you serve images in your server so that they can be served without authentication using express.static() pointed at a directory that contains only resources that can be served without authentication. If they do need to be authenticated, you may have to patch the loadImage() code to use the credentials: include option or load your images a different way.

Cookie not being stored in browser after call with Axios

I am playing with creating a simple authentication system and have a simple play project at: https://github.com/ericg-vue-questions/authentication
The main technologies I am using are:
FastAPI for the backend server
Vue for the frontend UI
axios to communicate from the frontend to the backend (eventually, a standard OpenAPI client)
I am make the axios call by doing:
axios
.get('http://127.0.0.1:8000/api/login', {
params: {
username: username,
password: password
},
withCredentials: true
})
.then((response) => {
console.log('Logged in')
console.log(response)
})
.catch(err => { console.log(err) })
On the backend, the code that sets the cookie is:
#app.get("/api/login", tags=["api"], operation_id = "login" )
async def login( username: str, password: str, response: Response ):
authenticated = False
if username in users:
if users[ username ][ 'password' ] == password:
authenticated = True
response.set_cookie( key="user", value=str( users[ username ][ 'id' ] ), max_age = 60 * 60 * 24 )
return authenticated
I can confirm that the cookie is being sent and received in the browser, but it is not being stored for some reason. I thought that the only thing that was required was to set withCredentials: true in the axios get request, but that doesn't seem to be working. Any idea what I might be doing wrong?
My Response headers are:
HTTP/1.1 200 OK
date: Sun, 15 Dec 2019 01:54:14 GMT
server: uvicorn
content-length: 4
content-type: application/json
set-cookie: user=userid; Max-Age=86400; Path=/
access-control-allow-credentials: true
access-control-allow-origin: http://localhost:8080
vary: Origin
I'm pretty sure that's because you are not setting a path, for example '/', to your cookie. That's the fourth parameter in set. If you're using your cookie in a https connection, you should also set a domain.

Does Axios support Set-Cookie? Is it possible to authenticate through Axios HTTP request?

I'm trying to authenticate express API back-end using Axios HTTP request call.
I was able to see 'Set-Cookie' in the response header, but cookie was not set. Is it possible to set cookies through Axios HTTP calls?
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 355
Content-Type: application/json; charset=utf-8
Date: Fri, 28 Sep 2018 05:59:01 GMT
ETag: W/"163-PAMc87SVHWkdimTJca7oRw"
Set-Cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; Max-Age=3.6; Path=/; Expires=Fri, 28 Sep 2018 05:59:04 GMT; HttpOnly
X-Powered-By: Express
Try this out!
axios.get('your_url', {withCredentials: true}); //for GET
axios.post('your_url', data, {withCredentials: true}); //for POST
axios.put('your_url', data, {withCredentials: true}); //for PUT
axios.delete('your_url', data, {withCredentials: true}); //for DELETE
For more information on this from the axios docs:
"withCredentials indicates whether or not cross-site Access-Control requests should be made using credentials" - https://github.com/axios/axios
More detail on withCredentials:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
Yes you can set cookies by Axios. The cookies need to be passed into the headers object. You can send cookies in a get/post/put/delete/etc. request:
As suggested by Aaron:
axios.get('URL', {
withCredentials: true
});
axios.post('URL', data, {
withCredentials: true
});
axios.put('URL', data, {
withCredentials: true
});
axios.delete('URL', data, {
withCredentials: true
});
Or you may also try this:
axios.get(url, {
headers: {
Cookie: "cookie1=value; cookie2=value; cookie3=value;"
}
}).then(response => {
console.log(response);
});
In case anyone else faces the problem I've had,
Here's a repost of my answer on a similar question https://stackoverflow.com/a/62821342/8479303
In my case, the network panel showed that the response had the 'Set-Cookie' header, but in axios the header wouldn't show up, and the cookie was being set.
For me, the resolution was setting the Access-Control-Expose-Headers header.
For explanation, from this comment on an issue in the axios repository I was directed to this person's notes which led me to set the Access-Control-Expose-Headers header -- and now the cookie is properly setting in the client.
So, in Express.js, I had to add the exposedHeaders option to my cors middleware:
const corsOptions = {
//To allow requests from client
origin: [
"http://localhost:3001",
"http://127.0.0.1",
"http://104.142.122.231",
],
credentials: true,
exposedHeaders: ["set-cookie"],
};
...
app.use("/", cors(corsOptions), router);
It was also important that on the axios side I use the withCredentials config in following axios requests that I wanted to include the cookies.
ex/
const { data } = await api.get("/workouts", { withCredentials: true });
I tried setting withCredentials: true but was still getting this error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:4000/users/register. (Reason: CORS request did not succeed).
CORS was configured to allow requests from the frontend port.
I had to change the default options for axios like so:
axios.defaults.withCredentials = true
And the issue was solved. No error and Set-Cookie working as expected.
cookie can't be touched, the thing is it gets bundled to request object after appended to the response object.
function sign(req,res){
res.cookie("x-token", signed, { maxAge: (new JWTService().jwtExpirySeconds *
1000) });
}
client after receiving this response just have to continue with requests, set-cookie in the name of "Cookie " will be bundled to those request, like this
caveat: when http cookie expires its is automatically removed and not bundled to request there after.

servicestack auth breaks at 4.0.21

I am encountering a problem when I upgraded my ServiceStack recently. I separated the different versions to find the problem started at v4.0.21. All earlier versions work and all later versions do not work. It only happens with my call to authenticate (with Basic Auth). Also, it only happens when I make the call to authenticate from our iPad app. Since we have not made any changes to our iPad app I know that something changed with ServiceStack that is causing our problem. I looked through the release notes and found there were a lot of changes to the auth capability due to the addition of a Windows Auth Provider and a couple of other new OAuth providers.
Is there some changes to the settings that I need to make that I am not seeing in the Release Notes? Has anyone else encountered this problem?
Here is our code for registering for ss-auth in global.asax.cs:
public override void Configure(Funq.Container container)
{
SetConfig(new HostConfig {
EnableFeatures = Feature.All.Remove(Feature.Metadata),
AllowJsonpRequests = false,
HandlerFactoryPath = "api"
});
var authFeature = new AuthFeature(
() => new AuthUserSession(),
new IAuthProvider[] {
new MyBasicAuthProvider() // override of BasicAuthProvider
}
);
authFeature.HtmlRedirect = null;
authFeature.IncludeAssignRoleServices = false;
Plugins.Add(authFeature);
container.Register<ICacheClient>(new AzureCacheClient("default"));
var userRepository = new InMemoryAuthRepository();
container.Register<IUserAuthRepository>(userRepository);
}
Here is the request URL:
POST https://vh.azurewebsites.net/api/auth?format=json
And here are the headers that went with the HTTP Request:
Host: vh.azurewebsites.net
Authorization: Basic dXN2dONjb3R0OnBhc3N3b3Jk
Accept-Encoding: gzip, deflate
Accept: application/json
Cookie: ss-opt=perm
Accept-Language: en;q=1, fr;q=0.9, de;q=0.8, ja;q=0.7, nl;q=0.6, it;q=0.5
Content-Length: 0
Connection: keep-alive
Proxy-Connection: keep-alive
User-Agent: mHealth/QA (iPad; iOS 7.1.1; Scale/1.00)
The auth provider was changed so that the implicit default /auth can now be used to tell if a user is authenticated which will return Session Info if a user is already authenticated or a 401 Unauthorized if they're not.
You need to call the explicit /auth/{provider} Auth Provider Route for the auth provider you wish to authenticate against which for Basic Auth is /auth/basic.