I'm trying to set an httpOnly cookie from my node.js api (localhost:3001) to work with my react client app (localhost:3000), everything I've tried so far results in no cookie being set in my browser. Some key factors about my setup:
Backend is node, running fastify, fastify-cookie & cors
// CORS
server.use(
require('cors')({
origin: ['https://localhost:3000'],
optionsSuccessStatus: 200,
credentials: true
})
)
// Cookies
server.register(require('fastify-cookie'), {
secret: process.env.JWT_SECRET
})
// Sending the cookie
reply
.setCookie('token', token, {
domain: 'localhost',
path: '/',
secure: true,
sameSite: 'lax',
httpOnly: true
})
.send({ user })
Client is running https localhost in chrome, making api calls using fetch.
const fetchUsers = async () => {
const req = await fetch(`${process.env.USERS_API_BASE}/users`, { credentials: 'include' })
const res = await req.json()
console.log(res)
}
Result
No cookie is ever set in my chrome application inspector, but it is sent to the browser from the server and looks correct.
set-cookie: token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEsImVtYWlsIjoiaGVsbG9Ac2hhbi5kaWdpdGFsIiwiaWF0IjoxNjIwNDI1ODI0LCJleHAiOjE2MjA0Mjk0MjR9.S8eOQMtSBY85wlenuxjIGYNuk3Ec5cKQ87pAhmCvQ9w.nfRxGzq3IMFimC%2FSJeUH9Xl7bH%2FyXVprwK1NBYfur4k; Domain=localhost; Path=/; HttpOnly; Secure; SameSite=Lax
request.cookies on the sever always returns a blank object {}. Any suggestions?
What you are facing is a CORS error OR at least it is categorized as one..
you see the server seems to think you're making a cross-domain request..
If you log the responce Headers this is typically what you would see
HTTP/1.1 200 OK
Date: Sun, 20 May 2018 20:43:05 GMT
Server: Apache
Set-Cookie: name=value; expires=Sun, 20-May-2018 21:43:05 GMT; Max-Age=3600; path=/; domain=.localHost
Cache-Control: no-cache, private
Access-Control-Allow-Origin: http://localHost:8080
Vary: Origin
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
Content-Length: 2
Keep-Alive: timeout=10, max=100
Connection: Keep-Alive
Content-Type: text/html; charset=UTF-8
but when you are making a request you kinda send it like this
const res = await axios({ method: 'POST', url: 'http://127.0.0.1:3000/api/v1/users/login', data: { email, password } });
Do you see the problem 127.0.0.1 != http://localhost:8000 and that is the solution to your problem
In short Check the Key=value Pair of Access-Control-Allow-Origin on your response and Request the domain names should match else the cookie won't be set on the browser...
Here is a GitHub Issue Link for this same problem
Related
I'm using the node.js module CSURF, which is configured to use cookies via cookie-parser.
For demo purposes, I'm just echoing the ANTI-CSRF token to the screen on a /form GET request. Here's the request and response via VS Code Rest Client plugin:
GET http://localhost:9000/form HTTP/1.1
User-Agent: vscode-restclient
accept-encoding: gzip, deflate
cookie: sid=s%3AYdAxaIHCvv38D6vd3VOi085SOzqkuZpN.eloHBwtgNm4yXQia3FtgR6puNj48kNZVbxlWtBZhSk0; _csrf=xdfFevA7j1qcGRo5BvB7JDQ2
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
X-DNS-Prefetch-Control: off
Expect-CT: max-age=0
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
Content-Type: application/json; charset=utf-8
Content-Length: 52
ETag: W/"34-4PDt3TpquKFR5AlQtYw1wqZJRD4"
Date: Wed, 03 Nov 2021 02:47:01 GMT
Connection: close
{
"csrfToken": "HhEOYbdx-lhbaEmFT_Udx-CyyZFvuXG2u3lI"
}
You can see the _csrf value in the cookie -- xdfFevA7j1qcGRo5BvB7JDQ2
Interestingly, this doesn't match the token output to screen -- HhEOYbdx-lhbaEmFT_Udx-CyyZFvuXG2u3lI
So I presume it's a cryptographic match, or a salt was added to the _csrf value to generate unique ANTI-CSRF tokens every time.
...which is fine, b/c CSURF works when I issue a POST request using HhEOYbdx-lhbaEmFT_Udx-CyyZFvuXG2u3lI.
The question/confusion comes into play when I issue a new GET request to the /form endpoint. The _csrf value (xdfFevA7j1qcGRo5BvB7JDQ2) doesn't change, only the ANTI-CSRF token that was output to the screen.
So it appears the ANTI-CSRF token changes on every request, but the cookie value doesn't. Is this correct behavior? It doesn't seem like it b/c I'd be able to always use any ANTI-CSRF token to bypass the check.
Here's the full code from CSURF URL https://www.npmjs.com/package/csurf:
var cookieParser = require('cookie-parser')
var csrf = require('csurf')
var bodyParser = require('body-parser')
var express = require('express')
// setup route middlewares
var csrfProtection = csrf({ cookie: true })
var parseForm = bodyParser.urlencoded({ extended: false })
// create express app
var app = express()
// parse cookies
// we need this because "cookie" is true in csrfProtection
app.use(cookieParser())
app.get('/form', csrfProtection, function (req, res) {
// changed original code to display token to screen instead of render it within a form; this is for dev purposes only
res.json({ csrfToken: req.csrfToken() })
})
app.post('/process', parseForm, csrfProtection, function (req, res) {
res.send('data is being processed')
})
Per OWASP (see this URL: https://security.stackexchange.com/questions/209993/csrf-token-unique-per-user-session-why), ANTI-CSRF token pairs should be changed on new sessions. So once I logged out (deleted my cookie), a new ANTI-CSRF token pair was created.
I wonder if I could change this time every time.
I'm trying to build a Node/Express server that essentially acts as a middleman server and logs any requests that come in, then forwards the request to the destination server, and forwards back any responses that come from the destination server. The goal is to be as "transparent as possible, making it seem as if there is no middleman server at all.
The problem I'm having is that my express server seems to be dumping in a bunch of unnecessary headers in the response from the destination server.
In my app.js I have some (a lot of) middlewares that are useful for my app in general but seems to inject headers in the response:
app.use(rateLimiter);
app.use(helmet());
app.use(xss());
app.use(mongoSanitize());
app.use(nocache());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
Then I have my middleware endpoint that receives the request, forwards it, and returns the response:
exports.createLink = async (req, res, next) => {
try {
const url = 'https://destination-endpoint.com';
const options = {
url: url,
};
request( options, function(err, remoteResponse, remoteBody) {
res.writeHead(remoteResponse.statusCode, {...remoteResponse.headers});
return res.end(remoteBody);
});
} catch (error) {
console.log(error);
next(error);
}
};
If I hit my middleman I get the response:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Vary: Accept-Encoding
Access-Control-Allow-Credentials: true
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
date: Sun, 24 Jan 2021 05:23:07 GMT
X-RateLimit-Reset: 1611465796
Content-Security-Policy: default-src 'self';base-uri 'self';block-all-mixed-content;font-src 'self' https: data:;frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests
X-DNS-Prefetch-Control: off
Expect-CT: max-age=0
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: no-referrer
X-XSS-Protection: 0
Surrogate-Control: no-store
cache-control: no-cache, private
Pragma: no-cache
Expires: 0
server: nginx/1.14.2
content-type: text/plain; charset=UTF-8
transfer-encoding: chunked
connection: close
x-request-id: 864b8443-5fe2-498e-8e88-662035afe6c7
x-token-id: d0cb94e2-9c87-4d6e-b32a-11fcc698ad2c
set-cookie: laravel_session=tBlSCeel0OFIR5pL9C6f02JfXGqoyg3SN6BH6jjG; expires=Sun, 24-Jan-2021 07:23:07 GMT; Max-Age=7200; path=/; httponly
{
"foo": "bar"
}%
While if I hit the endpoint directly I just get this:
HTTP/1.1 200 OK
Server: nginx/1.14.2
Content-Type: text/plain; charset=UTF-8
Transfer-Encoding: chunked
Vary: Accept-Encoding
X-Request-Id: 1f033b57-4a2f-48a1-82b3-41bce6e2d748
X-Token-Id: d0cb94e2-9c87-4d6e-b32a-11fcc698ad2c
Cache-Control: no-cache, private
Date: Sun, 24 Jan 2021 05:24:28 GMT
Set-Cookie: laravel_session=MdOGJ18iDU28XLmNYXf5T2RxSa25KxqFisxzCBzR; expires=Sun, 24-Jan-2021 07:24:28 GMT; Max-Age=7200; path=/; httponly
{
"foo": "bar"
}%
As you can see there is a lot of extra stuff added to the header. Some of it may be solvable by removing middlewares/etc. but I think the bigger issue is that I want to return the response from the destination server AS IS no matter what. I found that I can delete the current headers before sending it like this:
request( options, function(err, remoteResponse, remoteBody) {
const headers = res.getHeaders();
for (const head in headers){
res.removeHeader(head);
}
res.writeHead(remoteResponse.statusCode, {...remoteResponse.headers});
return res.end(remoteBody);
});
But that seems very heavy handed, there has to be an easier way to overwrite/set all response headers to exactly what I need. So my overall question is:
How do I return the response from the request to the destination server EXACTLY as is?
Why does the JWT Cookie disappear on page refresh?
I'm using Fetch to make a request
it initially sets the JWT cookie which I verify on Chrome Inspector
on page refresh, the cookie disappears. No specific errors are being logged.
Here's the frontend request:
$(form).submit(function(e) {
e.preventDefault();
var form_email = $('#email').val();
var form_password = $('#password').val();
var formData = {
email:form_email,
password:form_password,
}
const url = $(form).attr('action');
const options = {
method: 'POST',
body: JSON.stringify({
email:form_email,
password:form_password
}),
headers:{
'Content-Type':'application/json'
},
credentials:'include'
}
fetch(url,options)
.then(function(response) {
console.log(response);
})
});
On the server-side, I already have enabled CORS, set credentials to being true, and the origin.
const express = require("express");
const connectDB = require("./config/db");
const cors = require('cors');
const app = express();
connectDB();
app.use(cors({credentials: true, origin: 'https://zkarimi.com'}));
app.use(express.json({ extended: false }));
Here's how I handle the JWT signing and cookie response:
jwt.sign(
payload,
config.get("jwtSecret"),
{ expiresIn: 36000 },
(err, token) => {
if (err) throw err;
res.cookie('jwt',token, { httpOnly: true ,secure: true, sameSite:'None',maxAge: 3600000 })
res.json({user_email:email });
}
);
Lastly here's the response from the initial request:
HTTP/1.1 200 OK
Server: Cowboy
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: https://zkarimi.com
Vary: Origin
Access-Control-Allow-Credentials: true
Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoiNWZjODA1Zjk2MDRlNjEwMDE3NzQwZWUxIn0sImlhdCI6MTYwNzAyMjg5MSwiZXhwIjoxNjA3MDU4ODkxfQ.oXuA6DfdZbO2XW5HtLR44pVgGUWsXMyaV-l0iVeF7to; Max-Age=3600; Path=/; Expires=Thu, 03 Dec 2020 20:14:51 GMT; HttpOnly; Secure; SameSite=None
Content-Type: application/json; charset=utf-8
Content-Length: 30
Etag: W/"1e-vu/QJUeZrcotU6SR/PRLtu/c6Jw"
Date: Thu, 03 Dec 2020 19:14:51 GMT
Via: 1.1 vegur
After this request ^, refreshing the page removes the cookie from Application > Storage > Cookies and subsequent authentication requests indicate there's no JWT cookie.
In my node.js server I have included CORS as middleware like so:
app.use(cors({ origin: 'http://<CORRECT_ORIGIN_URL>:3030', credentials: true }))
I'm using Apollo Client in the app that sends the request, and have set credentials to 'include' when initialising ApolloClient, like so:
// Create a WebSocket link
const wsLink = process.browser ? new WebSocketLink({
uri: `ws://<CORRECT_REQUEST_URL>:8000/graphql`,
options: {
reconnect: true,
},
}) : null
// Create an http link (use batch, allow cookies response from server)
const httpLink = new BatchHttpLink({
uri: 'http://<CORRECT_REQUEST_URL>/api/',
credentials: 'include'
})
// Split terminating link for websocket and http requests
const terminatingLink = process.browser ? split(
({ query }) => {
const { kind, operation } = getMainDefinition(query)
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLink,
httpLink,
) : httpLink
// Create Apollo client
const client = new ApolloClient({
link: ApolloLink.from([authLink, errorLink, terminatingLink])
})
When I attempt to sign-in, I can see that a preflight OPTIONS request is sent and gets the correct response back:
Request Headers (OPTIONS request)
Access-Control-Request-Headers: content-type
Access-Control-Request-Method: POST
Origin: http://<CORRECT_ORIGIN_URL>:3030
Referer: http://<CORRECT_ORIGIN_URL>/login
Response Headers (OPTIONS request)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Origin: http://<CORRECT_ORIGIN_URL>:3030
Connection: keep-alive
Content-Length: 0
Date: Wed, 20 Mar 2019 03:09:14 GMT
Server: nginx/1.15.5 (Ubuntu)
Vary: Origin, Access-Control-Request-Headers
X-Powered-By: Express
Yet when the actual POST request is sent, I get the following response:
Response Headers (POST request)
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json
Date: Wed, 20 Mar 2019 03:09:15 GMT
Server: nginx/1.15.5 (Ubuntu)
Transfer-Encoding: chunked
Vary: Accept-Encoding, Origin
X-Powered-By: Express
I have no idea why the response headers are different in the post request when the options preflight show that it should be correct.
This incorrect POST response leads to the following error message on the client:
Access to fetch at 'http://<CORRECT_REQUEST_URL/api/' from origin
'http://<CORRECT_ORIGIN_URL>:3030' has been blocked by CORS policy:
The value of the 'Access-Control-Allow-Origin' header in the response
must not be the wildcard '*' when the request's credentials mode is
'include'.
I've tried googling and searching stackoverflow for a solution but can't find anything. Any ideas?
I solved my problem.
The issue is that Apollo Server adds CORS middleware by default, which was overriding my CORS settings. From Apollo's documentation:
Provide false to remove CORS middleware entirely, or true to use your
middleware's default configuration.
The default value is true.
To solve the issue, I simply had to disable CORS functionality in Apollo, which simply required setting cors: false in .applyMiddleware like so:
server.applyMiddleware({
app,
path: '/',
cors: false,
});
Further reference:
https://github.com/expressjs/cors/issues/134#issuecomment-413543241
I had similar issues with apache2 proxy running in front of my Express services. The proxy was caching some (only some!) of the responses. This is what I added to the apache config and it solved the problem:
Header set Cache-Control "no-cache, must-revalidate" env=no-cache-headers
Header set Pragma "no-cache" env=no-cache-headers
Header set Expires "Sat, 1 Jan 2000 00:00:00 GMT" env=no-cache-headers
I'm developing a web application and testing it using Google Chrome 60.0.3112.113.
To simplify the development process I use a node.js development web server with http-proxy-middleware to proxy my API request to the backend.
Now when I send a HTTP POST request using axios to one of the API endpoints to create a session in my backend, I get back slightly altered responses headers (copied from DevTools):
Direct response
HTTP/1.1 200 OK
Content-Length: 122
Content-Type: application/json
Set-Cookie: sessionid={4621f755-37da-41da-bdbd-9a6ce0ee02b7}; Max-Age=31536000; Version=1
Proxied response
HTTP/1.1 200 OK
X-Powered-By: Express
connection: close
content-length: 122
content-type: application/json
set-cookie: sessionid={4621f755-37da-41da-bdbd-9a6ce0ee02b7}; Max-Age=31536000; Version=1
Date: Thu, 07 Sep 2017 11:06:43 GMT
The problem is that chrome doesn't set the cookie specified in the proxied response (DevTools->Application->Storage->Cookies stays empty), however the direct response sets the cookie as expected.
Cookies are shown correctly in DevTools->Network->My Request->Cookies.
Both versions (direct and proxied) are being accessed via http://localhost:[8080 / 3000]
Could the lowercase set-cookie header be ignored in chrome?
Or could the other headers interfere with setting of the cookie?
Btw: Works fine in Safari 10.1.2 (12603.3.8)
// proxy middleware options
var options = {
target: 'http://localhost:8081', // target host
changeOrigin: true, // needed for virtual hosted sites
ws: true, // proxy websockets
logLevel: "debug",
pathRewrite: {
'^/src/api/' : '/api/'
},
onProxyRes: function (proxyRes, req, res) {
if (proxyRes.headers['set-cookie'] != undefined) {
req.session['cookie'] = proxyRes.headers['set-cookie']; // must be or you will get new session for each call
req.session['proxy-cookie'] = proxyRes.headers['set-cookie']; // add to other key because cookie will be lost
}
console.log("response: " + req.session.id);
console.log(req.session);
},
onProxyReq: function (proxyReq, req, res) {
// check for whether the session be freshed
if (req.session.view)
req.session.view ++;
else
req.session.view = 1;
// use ower key to restore cookie
if (req.session['proxy-cookie'] != undefined)
proxyReq.setHeader('cookie', req.session['proxy-cookie'][0]);
console.log("request: " + req.session.id);
console.log(req.session);
}
};
Set cookieDomainRewrite: 'localhost'