Localhost frontend no longer sending cookie to development backend - cookies

I'm trying to connect my local frontend to our development backend hosted in aws.
Everything used to work, and I'm going crazy trying to figure out what happened.
The issue is that the request to the backend isn't passing along the cookie we use for authentication.
We have cors setup and it appears to be working correctly. The Options call returns everything I'd expect
.
but the request just doesn't contain the cookie.
I'm setting the cookie via javascript in the frontend code rather than having the server itself set it. This setup used to work idk why it doesn't anymore.
What are the reasons why a browser wouldn't pass a cookie along?
My checklist includes:
ensuring Access-Control-Allow-Credentials is passed back from the Options request
ensure withCredentials is set on the frontend making the request
ensuring the cookie domain is set to /
We recently added some CSRF protection but I disabled that and still can't get the cookie to be sent.
A soapui call to the backend works just fine.

The issue lied in the samesite cookie.
I deployed my development server to explicitly set samesite=none and things are working again.

axios({
method: "get",
withCredentials: true,
});
adding withCredentials:true worked for me

Related

Keystone session cookie only working on localhost

Edit:
After investigating this further, it seems cookies are sent correctly on most API requests. However something happens in the specific request that checks if the user is logged in and it always returns null. When refreshing the browser a successful preflight request is sent and nothing else, even though there is a session and a valid session cookie.
Original question:
I have a NextJS frontend authenticating against a Keystone backend.
When running on localhost, I can log in and then refresh the browser without getting logged out, i.e. the browser reads the cookie correctly.
When the application is deployed on an external server, I can still log in, but when refreshing the browser it seems no cookie is found and it is as if I'm logged out. However if I then go to the Keystone admin UI, I am still logged in.
In the browser settings, I can see that for localhost there is a "keystonejs-session" cookie being created. This is not the case for the external server.
Here are the session settings from the Keystone config file.
The value of process.env.DOMAIN on the external server would be for example example.com when Keystone is deployed to admin.example.com. I have also tried .example.com, with a leading dot, with the same result. (I believe the leading dot is ignored in newer specifications.)
const sessionConfig = {
maxAge: 60 * 60 * 24 * 30,
secret: process.env.COOKIE_SECRET,
sameSite: 'lax',
secure: true,
domain: process.env.DOMAIN,
path: "/",
};
const session = statelessSessions(sessionConfig);
(The session object is then passed to the config function from #keystone-6/core.)
Current workaround:
I'm currently using a workaround which involves routing all API requests to '/api/graphql' and rewriting that request to the real URL using Next's own rewrites. Someone recommended this might work and it does, sort of. When refreshing the browser window the application is still in a logged-out state, but after a second or two the session is validated.
To use this workaround, add the following rewrite directive to next.config.js
rewrites: () => [
{
source: '/api/graphql',
destination:
process.env.NODE_ENV === 'development'
? `http://localhost:3000/api/graphql`
: process.env.NEXT_PUBLIC_BACKEND_ENDPOINT,
},
],
Then make sure you use this URL for queries. In my case that's the URL I feed to createUploadLink().
This workaround still means constant error messages in the logs since relative URLs are not supposed to work. I would love to see a proper solution!
It's hard to know what's happening for sure without knowing more about your setup. Inspecting the requests and responses your browser is making may help figure this out. Look in the "network" tab in your browser dev tools. When you make make the request to sign in, you should see the cookie being set in the headers of the response.
Some educated guesses:
Are you accessing your external server over HTTPS?
They Keystone docs for the session API mention that, when setting secure to true...
[...] the cookie is only sent to the server when a request is made with the https: scheme (except on localhost)
So, if you're running your deployed env over plain HTTP, the cookie is never set, creating the behaviour you're describing. Somewhat confusingly, in development the flag is ignored, allowing it to work.
A similar thing can happen if you're deploying behind a proxy, like nginx:
In this scenario, a lot of people choose to have the proxy terminate the TLS connection, so requests are forwarded to the backend over HTTP (but on a private network, so still relatively secure). In that case, you need to do two things:
Ensure the proxy is configured to forward the X-Forwarded-Proto header, which informs the backend which protocol was used originally request
Tell express to trust what the proxy is saying by configuring the trust proxy setting
I did a write up of this proxy issue a while back. It's for Keystone 5 (so some of the details are off) but, if you're using a reverse proxy, most of it's still relevant.
Update
From Simons comment, the above guesses missed the mark 😭 but I'll leave them here in case they help others.
Since posting about this issue a month ago I was actually able to work around it by routing API requests via a relative path like '/api/graphql' and then forwarding that request to the real API on a separate subdomain. For some mysterious reason it works this way.
This is starting to sound like a CORS or issue
If you want to serve your front end from a different origin (domain) than the API, the API needs to return a specific header to allow this. Read up on CORS and the Access-Control-Allow-Origin header. You can configure this setting the cors option in the Keystone server config which Keystone uses to configure the cors package.
Alternatively, the solution of proxying API requests via the Next app should also work. It's not obvious to me why your proxying "workaround" is experiencing problems.

Why can't I see my (localhost) cookie being stored in Electron app?

I have an Angular app using Electron as the desktop wrapper. And there's a separate Django backend which provides HTTP APIs to the Electron client.
So normally when I call the login API the response header will have a Set-Cookie field containing the sessionId. And I can clearly see that sessionId in Postman, however, I can't see this cookie in my Angular app (Dev tools of Electron).
After some further debugging I noticed a warning sign beside my Set-Cookie in dev tools. It said that the cookie is blocked due to the SameSite being set to Lax. So I found a way to modify the server code to return a None samesite (together with a Secure property; I'm using HTTP):
# settings.py
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_SAMESITE = 'None'
which did work (and the warning sign is gone) but the cookie is still not visible.
So what's the problem here? Why can't (and How can) I see that cookie so as to make sure that the login works in the actual client, not just Postman?
(btw, now both ends are being developed in localhost.)
There's no need to worry. A good way to check if it works is to actually make a request that requires login (after the API has been Postman tested) and see if the desired data are returned. If so, you are good to go (especially when the warning is gone).
If the sessionId cookie is saved it should automatically be included in the request. Unless there's something wrong with the cookie's path; but a / path would be fine.
Why is the cookie not visible: it's probably due to the separation of front and back ends. In Electron, the pages are typically some local HTML files, as one common step during configuration is to probably modify loadURL or something like that in main.js, for instance:
mainWindow.loadURL(`file://${__dirname}/dist/your-project/index.html`);
So the "site" you are accessing from Electron can be considered as local filesystem (which has no domain and hence no cookie at all), and you should see an empty file:// entry in dev tools -> application -> storage -> cookie. It doesn't mean a local path containing all cookies of the Electron app. Although your backend may be on the same local machine, you are accessing as http:// instead of file:// so the browser (Electron) will treat it as an actual web server.
Therefore, your cookies should be stored in another entry like http(s)://localhost and you can't see it in Electron. (Note that the same cookie will work in both HTTP and HTTPS)
If you use Chrome instead to test, you may be able to see it in all cookies. In some cases where the frontend and backend are deployed to the same host you may see the cookie in dev tools. But I guess there're always some reasons why you need Electron to create a desktop app (e.g. Python scripts).
Further reading
Using HTTPS
Although moving to HTTPS does not necessarily solve the original problem, it may be worth doing in order to prevent potential problems and get ready for the publish.
In your case, for the backend, you can use django-sslserver as a temporary solution before getting your SSL, but it uses a self-signed certificate and may make your frontend complain.
To fix this, consider adding the following code to the main process:
# const { app } = require('electron');
if (!app.isPackaged) {
app.commandLine.appendSwitch('ignore-certificate-errors');
}
Now it provides a good way to distinguish between development (unpacked) and production (packed) and only disables certificate check in development in order to make the code work.
Assuming that SESSION_COOKIE_SECURE in your config refers to cookie's secure flag, You ll have to set
SESSION_COOKIE_SECURE = False
because if this flag is set to True the browser will allow this cookie to be set only if you are using an https connection.
PS: This is just for your localhost. Hopefully you ll be using an Https connection in other environments.

Cookie not being set on angular client

I have a backend app in django python and it is being served on http://localhost:8000.
I have a angular frontend which is being served on http://localhost:4200.
I have disabled CORS on django.
On hitting the login api on http://localhost:8000/auth/login/, I am getting a valid response
along with the Set-Cookie header.
Here is my angular code to print the cookies:
this.http.post<any>('http://localhost:8000/auth/login/', this.LoginForm, { observe: 'response' }).subscribe(response => {
console.log("response is ", response);
var cookies = this.cookieService.getAll();//('cookies');
console.log("cookies is :", cookies);
It prints an empty object on console.
How do I make this work? I want to use cookies for authentication.
You are trying to set cross domain cookies, which will not work straight away. There are a few steps to follow to be able to do that.
Set withCredentials: true when making the authentication request from angular
this.http.post<any>('http://localhost:8000/auth/login/', this.LoginForm, { observe: 'response', withCredentials: true })
Configure your server to return the following CORS headers: Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: http://localhost:4200
Note
One of the cookies that you are setting is HttpOnly. As such, you cannot access it from Javascript (see documentation).
You may not need to access the cookies with JS anyway. If you just want to send the cookies in the next API requests, just pass withCredentials: true to HttpClient other api calls
this.http.get('http://localhost:8000/path/to/get/resource',
{ withCredentials: true }).subscribe(response => {
Set-Cookies:
In the example in the Question, both client and server are in the same domain, localhost.
On deployment, this may not be the case.
Let us assume the domains as below,
Client : client1.client.com
Server: server1.server.com
A http request from the Angular web app in client1.client.com to https://server1.server.com/api/v1/getSomething has Set-Cookie: JSESSIONID=xyz in the response header.
The cookie will be set on server1.server.com and NOT on client1.client.com.
You can enter server1.server.com in the URL bar and see the cookie being set.
withCredentials:
There is no need for the angular app to read the cookie and send it in the following requests. withCredentials property of http request can be used for this.
Refer: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
Example:
public getSomething(): Observable<object> {
const httpOptions = {
withCredentials: true
};
return this.http.get(`${this.serverUrl}/getSomething`, httpOptions);
}
Refer: https://angular.io/api/common/http/HttpRequest
withCredentials will set the cookies from the server's domain in the requests to the server.
As mentioned before Set-Cookie: JSESSIONID=xyz in the response from server1.server.com will be set in server1.server.com. The Angular app in client1.client.com need not read it. withCredentials will take care of it.
cross domain issues:
When the server and client are in different domains, using withCredentials may not work in all browsers, as they are considered as third party cookies.
In my recent testing on May 2020, I found that withCredentials is not working in certain browsers when the client and server are in different domains.
In Safari, the issue occurs when "Prevent cross-site tracking" is enabled (by default). The issue is prevented by disabling the same. https://support.apple.com/en-in/guide/safari/sfri40732/mac
In Android apps, the issue can be avoided by using Chrome Custom Tabs instead of Android WebView. https://github.com/NewtonJoshua/custom-tabs-client , https://developer.chrome.com/multidevice/android/customtabs
Same domain:
Looks like mainstream browsers are moving to block third-party cookies.
Safari - Full Third-Party Cookie Blocking and More
Chrome (by 2022) - Building a more private web: A path towards making third party cookies obsolete
The solution is to have both the client and server in the same domain.
Client: client1.myapp.com
Server: server1.myapp.com
And in the Set-Cookie response include the root domain too.
Example: "JSESSIONID=xyz; Domain=.myapp.com; Path=/"
This will make sure the cookies are set in all cases.

Debugging with local instance and authentication cookies

So, I've recently taken over the front-end of a project in which the previous front-end developer always did his debugging with a localhost instance connecting to a remote staging back-end.
Right now we are improving some security issues and are using CSRF tokens.
For each POST,PUT,DELETE request, I'm first GETting a csrf endpoint, which sets a JSESSIONID HttpOnly cookie and returns a CSRF token in the response body. For the subsequent request the CSRF token goes into the request header and the cookie of course gets sent automatically of course.
Now,.. my code works fine deployed on the remote staging front-end. But, this new functionality has totally prevented me from debugging with a local instance, because the cookie wont work when I GET the CSRF token from localhost, because this is a different domain of course.
This requires me to deploy every single change of code to the staging front end. Very uncomfortable workflow when performing the usual trial-and-error fix.
Adding the remote as a localhost alias to my hosts file also doesn't work, because this routes all my requests to the remote to my own machine, which doesn't run a local instance of the remote.
I would've thought there would be a Chrome extension or something like that for problems like these, but since I couldn't find any I wondered if I'm missing a very obvious point here.
Okay, it seems that this was a really general issue:
set withCredentials to true for the request to the remote and use a chrome plugin to overwrite the response headers to:
Access-Control-Allow-Origin: http://localhost
Access-Control-Allow-Credentials: true

Where do HTTP request cookies originate?

I have a VB.NET app that sends a POST request to a script on my server that is running Cloudflare. I always get an error when sending the request from the app, however using a Firefox extension to simulate the request works fine. With the use of Fiddler I think I have found the cause of the problem:
When sending the request with the Firefox addon an extra header is attached to the request:
Cookie: __cfduidxxxxxxxxxxxx
This cookie is from Cloudflare, but where does it come from, ie. how can I get this cookie value and send it with my requests from the VB app? I tried copying and pasting the cookie into the app and it worked fine, so this leads me to conclude that I need this cookie, however this value is unique for each user so I cannot simply hardcode it into the app.
Quick side-note: Not sure if this helps, but if I send a GET request from the VB app it works fine without the __cfduid cookie.
Look for a Set-Cookie header coming back from the server on it's response. It will expect to get that value back on subsequent requests in a Cookie: header. This value is usually an opaque string that is classified by a path, although not always.