Cannot set cookie in client from a Go API - http

I have a backend written in Go, hosted on Heroku, let's call it, https://foo.herokuapp.com. I have a frontend hosted on a different domain, let's call it, https://ui.example.com.
The backend API has an endpoint /api/user/login which sends back a JSON Web Token in the form of cookie shown as below:
http.SetCookie(w, &http.Cookie{
Name: "token",
Value: token, // the JWT
HttpOnly: false, // for testing, set to true later once I fix this
MaxAge: int(time.Hour * 24 * 3),
Expires: time.Now().UTC().Add(time.Hour * 24 * 3),
Path: "/",
Secure: true,
SameSite: http.SameSiteNoneMode,
})
These are my CORS settings on the server.
crossOrigin := cors.New(cors.Options{
AllowedOrigins: []string{allowedOrigin},
AllowCredentials: true,
AllowedMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut},
})
The frontend makes a request to the backend as given below.
const endpoint = "/api/user/login/"
fetch(host + endpoint, {
method: "POST",
credentials: 'include',
body: JSON.stringify({
email,
password
})
}).then((response) => console.log(response))
.catch((err) => console.log(err));
PROBLEM:
Now this cookie is actually visible in my browser's Network tab.
But the cookie does not exist in the application tab (or Storage tab in firefox where cookies exist). The browser is not saving the cookie, which is causing the subsequent requests to fail as the token in the cookie is verified and decoded before processing the actual request.
In another somewhat related thread, I got to know that Heroku terminates SSL before reaching my app. And, thus secure cookies cannot be set for non-SSL traffic. The solution there suggests trusting the scheme found in X-Forwarded-For. I enabled that using using the https://github.com/gorilla/handlers package as follows.
// srv is my actual handler
// crossOrigin is the CORS middleware
// finally wrapped by ProxyHeaders middleware
handlers.ProxyHeaders(crossOrigin.Handler(srv))
Yet this is not working.
I read many threads/blogs. Nothing has worked so far. What am I doing wrong?

Cookies are saved by the browser for the domain which set the cookie in the first place. Only becasue you can't see the cookie in the Application tab, does not mean that the cookie wasn't saved.
If your frontend https://ui.example.com makes an XHR call to https://foo.herokuapp.com and that call returns a Set-Cookie header, then the browser saves that cookie under foo.herokuapp.com domain. You will not see it in the ui.example.com's Application tab. Still, when you make another XHR call to foo.herokuapp.com then the browser will send the cookies that you've set earlier.
You can make this experiment: After logging in, open a new tab and navigate to https://foo.herokuapp.com. Now open the Application tab and you should see your cookies there.
That said, remember that the browser will treat these cookies as 3rd party cookies, and browser vendors will eventually drop support for 3rd party cookies. Eventually you should make sure that your frontend and backend are served from the same parent domain.
As for the other problem - Heroku's termination of SSL between their gateway and your app is not a problem. The secure flag on a cookie is an information for the browser - the browser will not accept or send a cookie with this flag over a non-SSL connection. The connection between your browser and the heroku server is SSL, so cookies will be accepted/sent. In your backend, cookies are just HTTP headers, and the backend does not really care neither about the cookies' flags nor by the connection type.

Related

Express module cookie-session not including SameSite and Secure in Response Set-Cookie

Seen this before here, but I've seen no real resolution. The server's Node Express express-session module OR cookie-session module sends back a Session Cookie, but as I had not coded in the SameSite/Secure attributes, they are not there and do the client on a subsequent POST to the server fails as Not Logged In, with a 403. As expected.
First, my client logs in to the server successfully:
Here is the corresponding server code, using express-session:
Which produced a Session Cookie via the Set-Cookie. NOTICE the SameSite='none' and Secure=true attributes were not included, and as expected, not there.
Now, I have added the sameSite and secure attributes to the session object and run the Login again.
Lets look at the Response Headers returned from this Login, with the attributes added to the session object. Not only do we not see the attributes on the Set-Cookie Response Header, but there is NO cookie returned!
It appears that when these 2 attributes are added to the session object in either express-session or cookie-session that the result is no cookie is returned. The meaning being that a subsequent POST to the server will return a 403, User Not Logged In.
I'm really stumped. I've spent a LOT of time on this! Thank you for ideas and help.
You seem to be misusing the cookie-session middleware. The cookieSession function takes an JavaScript object but the documentation doesn't mention any cookie field in that object.
Anything specified in a cookie field is ignored by the middleware and has no effect on the resulting cookie; the only reason your cookie ended up being flagged HttpOnly is that it's the middleware's default behaviour.
Instead, all the cookie attributes should be specified in a "flat" object, like so:
app.use(cookieSession({
name: 'session',
secret: secret,
domain: 'chicagomegashop.com',
sameSite: 'none',
secure: true,
httpOnly: true
}));
However, you have another issue. If I'm interpreting your screenshots correctly, you seem to attempt to set a cookie with a Domain attribute of chicagomegashop.com in a response from https://paylivepmt.com. That cannot work; browsers will ignore such a Set-Cookie response header:
The user agent will reject cookies unless the Domain attribute specifies a scope for the cookie that would include the origin server.

Flask-JWT-Extended set cookies with double submit cookie method, prevent HTTP-only cookie

I'm using Flask-JWT-Extended and double submit cookie method from there for my Flask backend and React Frontend. So when user logs in from frontend, backend sets total of 4 different cookeis: csrf_access_token, csrf_refresh_token, access_token_cookie, refresh_token_cookie. Out of these 4 cookies, access_token_cookie and refresh_token_cookie should be HTTPonly cookie, and thus not accessible by JS and csrf_access_token and csrf_refresh_token are non-HTTPonly cookie. So the idea here is that HTTPOnly cookie holds user's session information with CSRF token and non-HTTPonly cookie holds the CSRF token and when POST request is made, CSRF token accessed by JS is sent to backend along with the other cookies.
This was working just fine in my development environment, two of the cookies were accessible by JavaScript and thus I could send csrf_acccess_token along with the request with withCredentials True, but when I deploy this to test environment with TLS using Nginx (Both backend and frontend), it is setting all 4 cookies as HTTPOnly cookie, and thus, I cannot make any POST request.
I'm not sure whether this was caused by the Nginx, but from what I can tell, I don't see much options to turn off 2 of the HTTPOnly cookies being registered from the backend.
Below is my configuration for flask-jwt-extended
CORS_HEADERS = "Content-Type,X-CSRF-TOKEN"
JWT_TOKEN_LOCATION = ["cookies"]
JWT_COOKIE_SECURE = True
#JWT_COOKIE_SAMESITE = None
JWT_ACCESS_TOKEN_EXPIRES = 600
JWT_REFRESH_TOKEN_EXPIRES = 1200
JWT_CSRF_IN_COOKIES = True
JWT_COOKIE_DOMAIN = ".mydomain.com"
#JWT_ACCESS_COOKIE_PATH = '/'
#JWT_REFRESH_COOKIE_PATH = '/'
JWT_COOKIE_CSRF_PROTECT = True
JWT_SECRET_KEY = "secret"
Any advice would be greatly appreciated!
Flask-JWT-Extended should never be setting the csrf cookies as httponly. I wonder if there is an nginx setting that is converting all cookies to httponly (something like proxy_cookie_path)?
If that’s the case, another approach you could take it to set JWT_CSRF_IN_COOKIES to false, and use https://flask-jwt-extended.readthedocs.io/en/stable/api/#flask_jwt_extended.get_csrf_token to grab the csrf token when a JWT is created, return it as part of the JSON payload, and store it in localStorage instead of in those non-httponly cookies so that your JavaScript can still grab it when making requests.

How to access cookies inside a Cypress request to a different domain?

I'm trying to authenticate at a different domain as part of a Cypress test using cy.request. The authentication request needs to contain the value of the XSRF-TOKEN cookie as a header. This is easily solved when on the same baseURL as the authentication domain: visit the domain, read the cookie via cy.getCookie, then make the request. Since cypress does not allow visiting multiple domains in a single test a different workflow is needed.
My solution right now is to replace the initial cy.visit with a cy.reqeuest (this sets the cookie as verified by looking at the request headers of the second request). However, I can't figure out how to read this cookie before I make the second authentication request. cy.getCookies() is empty, and document.cookie is empty. The response of the cy.request only contains a "set-cookie" header the first time, and I can't figure out how to read the default headers of the cy.request since cy.intercept does not work with cy.request.
Sketch of my attempt (where c.value is null):
cy.request({url: "https://notbaseurl/login"}).then(res => {
cy.getCookie("XSRF-TOKEN").then(c => {
cy.request({url: "https://notbaseurl/auth", method: "POST", headers: {"X-XSRF-TOKEN": c.value}})
})
})
This is not currently possible. See https://github.com/cypress-io/cypress/issues/8956

Angular 5 wont send auth cookie back

I have an Angular 5 app and an ASP.net core 2.0 rest api. When they are both running on localhost, my authentication works just fine. I have recently deployed the rest api on another server and suddenly I can no longer authenticate. I have CORS enabled on the server via:
app.UseCors(options =>
{
options.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin().AllowCredentials();
});
In Angular, I setup my request options:
var headers: Headers = new Headers();
headers.append('Content-Type', 'application/json')
var requestOptions = new RequestOptions({
headers: headers,
withCredentials: true,
});
Using fiddler, I watch my login POST request and see that the Response contains:
Set-Cookie: .AspNetCore.Identity.Application=CfD...;expires=Thu, 16 Aug 2018 21:25:02 GMT; path=/; samesite=lax; httponly
However, after the redirect back to the home page, I make an api request to see if a user is logged in
var url = this.runtimeConfig.getConfig().apiUrl + `api/Auth/IsUserLoggedIn/`;
return this.http.get(url, requestOptions).toPromise().then(response => {
return <BoolDTO>ServerMessages.deserialize(response);
});
Looking at fiddler, this GET request does not contain a COOKIE header as it did when both apps were on localhost.
Odder still, if I then paste the url http://<serverip>:<port>/api/Auth/IsUserLoggedIn/ into my browser's address bar, it does send the COOKIE header and says that a user is logged in.
Is there something I need to do to get Angular's http client to store and resend the cookie?
It looks like it was the samesite=lax setting on the cookie. It seems odd that "lax" would restrict this scenario, but the documentation for ASP.net's SameSiteMode.LAX says:
LAX - The cookie will be sent with "same-site" requests, and with "cross-site" top level navigation.
Since XHR requests are not top level navigation, it would not send the cookie. The solution to this is to remove the SameSite from the cookie on the server in ConfigureServices
services.ConfigureApplicationCookie(options =>
{
options.Cookie.SameSite = SameSiteMode.None;
});
I should mention that doing this opens your site to CSRF attacks, but if you want a client side frontend hosted on a different server than a REST API, this seems to be the only way. I will likely re-work my project so that the front and back end are hosted on the same server.

How can I get my express server to redirect the client to a different domain using cors?

I have an express server that already has cors middlewear enabled.
https://myapi.com
app.use(cors({ origin: true }));
I have a single page application that makes a request and is suppose to get redirect to paypal after. (It gets served from a different origin as listed below)
https://myAngularApp.com (some service)
http.post('https://myapi.com/create-payment', data);
So back in in the express server, I want to send them off to paypal for authentication:
app.post('/create-payment', (req, res) => {
res.redirect('https://www.sandbox.paypal.com/somewhere..');
})
Back in the client I get the following error:
Failed to load https://www.sandbox.paypal.com/somewhere: Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'null' is therefore not allowed access.
Looking at the request my client makes to paypal, you can also see that the origin is null.
(Just to note, disabling app.use(cors({ origin: true })); won't allow the client to get a normal response from the server, so this already shows that the cors middleware is linked up.
Error when commenting out cors
// app.use(cors({ origin: true })); - Commented Out
Failed to load "https://myapi.com/create-payment": Redirect from
'https://myapi.com/create-payment' to
'https://www.sandbox.paypal.com/somewhere.' has been blocked by CORS
policy: No 'Access-Control-Allow-Origin' header is present on the
requested resource. Origin 'https://myAngularApp.com' is therefore not
allowed access.
What else do I need to setup on the express server so that the client can be redirected to paypal?
Redirects work like this:
Client makes HTTP request
Server makes HTTP response that includes an instruction to request a different URL
Client makes HTTP request to the different URL
Server (possibly a different server) makes HTTP response
If, at step 2, the server grants permission to read the response via CORS, then that grants permission for that request.
There is no way for the response at step 2 (which is being made by your server) to grant permission to read the response at step 4 (which is being made by PayPal's server).
If Paypal doesn't grant permission with CORS, then your JavaScript cannot read the response.
(Just imagine if that weren't the case: EvilHacker.Net grants permission with CORS, then redirects to GMail.com, and then EvilHacker can read all your email!)

Resources