Cookie not set in request with NodeJS and NextJS - javascript

I'm developing a fullstack app with Node + Express backend and NextJS front end (separate servers) and am having trouble requesting the browser to attach the cookie vended down as part of the response header from the node server. Here's the setup:
Node server is running on localhost:3000 and NextJs server is running on localhost:3001.
I have set up alias in etc/hosts to route someAlias.com to 127.0.0.1.
Using the front end UI (port 3001) I was able to vend the cookie with JsHttp's cookie module with the following code from the backend (port 3000):
import { serialize } from 'cookie';
...
const cookie = serialize(TOKEN_NAME, TOKEN_VAL, {
httpOnly: true,
sameSite: 'none',
});
I was able to observe the Set-Cookie header in the response.
However, in the subsequent requests, I did not see the cookie being attached. I have tried fiddling with the above cookie serialization params with no success:
Here are the arguments I've tried:
domain: ['.someAlias.com:3000', '.someAlias.com:3001']
path: '/'
domain: '.someAlias.com'
I have a feeling it might just be due to front end and back end server ports being different, but all requests have been initiated on the client side going to localhost:3000 (backend port). So not sure what I've possibly done wrong here.
====== UPDATE =======
I've run a couple more experiments, and found out that when I'm accessing a URL directly, NextJs renders the page server-side. When I'm transitioning between pages within the app, the page is rendered client-side where it queries the backend port 3000 directly. Unfortunately in neither scenario did I see any cookie being set...

the cookies must be sent directly from the browser to the server , which is not the case when you use nextJs . because when you access to your app next js will server side render your page and then the request will be sent from nextjs server to your nodejs server so the browser will send the cookies to nextjs server not to your nodejs server .
the solution is to send cookies manually from nextjs server to nodejs .
example with fetchApi and getServerSideProps function:
export async function getServerSideProps(context){
try{
const res = await fetch(`your-api-endpoint`, {
method: 'GET',
credentials:'include',
headers: {
'Access-Control-Allow-Credentials': true,
Cookie: context.req.headers.cookie
},
})
const data = await res.json()
if(!res.ok){
throw data
}
}catch(err){
return console.log(err)
}
return {
props: {
data
}
}
}

SAMESITE NONE
If you use SameSite=none you also need to use Secure, meaning SSL must also be used. You are then saying that your 2 servers are from unrelated domains, which is probably not what you want, since browsers will drop cookies aggressively.
LOCAL PC OPTIONS
Use these settings initially on your development computer, and ensure that all URLs used in the browser and Ajax calls use http://somealias.com:3000 and http://somealias.com:3001 rather than localhost. Cookies will then stick on a development computer.
Domain=.somealias.com
Path=/
SameSite=strict
HTTP Only
DEPLOYED OPTIONS
When you deploy to a proper environment, also use SSL and set the cookie option Secure. Most importantly, the two domains must meet hosting prerequisites of sharing the same base domain, eg:
https://www.example.com
https://api.example.com
This ensures that cookies issued are considered first party and in the same site, so that they are not dropped. If preconditions are not met, there is nothing you can do in code to fix the problem.
SIMILAR RESOURCE
This Curity code example uses local development domains and same site cookie settings similar to those I've used above and may be useful to compare against.

You should set serialized cookie with res.set Express method.
Alternatively, you can use res.cookie method without additional cookie package like this:
res.cookie(TOKEN_NAME, TOKEN_VAL, {
httpOnly: true,
sameSite: 'none',
});
Note, you shouldn't worry about different ports on the same domain, since cookies are not isolated by port but domain only. No matter what port you use, cookies should be visible.

Since you said, "I was able to observe the Set-Cookie header in the response", I believe your node.js setting correct.
When you get response from node js, you need to set cookies, which can be done with a npm packages easily. I will demonstrate with js-cookie:
import Cookies from "js-cookie";
you write a reusable function to set the cookies:
// what ever your project returns
setSession(authResult) {
//converting everything to miliseconds
const expiresAt =
JSON.stringify(authResult.expiresIn * 1000) + new Date().getTime();
// I just put properties. I dont know how project sets
Cookies.set("user", authResult.idTokenPayload);
Cookies.set("jwt", authResult.idToken);
Cookies.set("expiresAt", expiresAt);
}
Everytime you make request you have to set headers. You have to retrieve cookies based on if you are on browser or on server. So you have to write a function if you are on server. Since I demonstrated how to set cookies with js-cookies, you can get the cookies easily on the browser. This reusable function to retrieve the cookie if you are on the server:
// cookieKey: I set three "user", "jwt","expiresAt"
export const getCookieFromReq = (req, cookieKey) => {
console.log("req.headers", req.headers);
// cookies are attached to the req.header.cookie.
const cookie = req.headers.cookie
.split(";")
.find((c) => c.trim().startsWith(`${cookieKey}=`));
if (!cookie) return undefined;
return cookie.split("=")[1];
};
Now you have to write a function to set the headers:
import Cookies from "js-cookie";
import { getCookieFromReq } from "./directoryOf";
export const setAuthHeader = (req) => {
const token = req ? getCookieFromReq(req, "jwt") : Cookies.getJSON("jwt");
if (token) {
return {
headers: { authorization: `Bearer ${token}` },
};
}
return undefined;
};
Now when you make request, you have to use this setAuthHeader. For example:
await axiosInstance
.post("/blogs", blogData, setAuthHeader())
.then((response) => response.data)
.catch((error) => rejectPromise(error));

Related

Setting Cookies does not work in production Vuejs, vuecookies

Im using Vue.js 3 & the vue-cookies package Vue Cookies
this is how im setting the cookies in the app
in main.js
import VueCookies from "vue-cookies";
app.use(VueCookies, { expires: "35min" });
in login.vue
const $cookies = inject("$cookies");
const handleLogin = async () => {
try{
const res = await axios({
method: "POST",
url: "/sellers/login",
data: {
Email: email.value,
Password: password.value,
},
// withCredentials: true,
});
let token = res.data.token;
// $cookies.set("jwt", token); //---->method 1
const storeToken = useLocalStorage("token", {
token: res.data.token,
});
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});
let storedToken = JSON.parse(window.localStorage.getItem("token"));
$cookies.set("jwt", storedToken.token); ///---> method 2
console.log("getting the cookie");
let y = $cookies.get("jwt");
console.log(y);
}
catch(error){
}
i've tried storing the cookie in local storage then retrieving and setting it from there (method 2) because i thought the problem was method 1
the results of console.log(y) is null
however,i have confirmed the token is in local storage
Both Method 1 & 2 work when the app is running via the Vite development server
After building for production and serving the assets in dist with nodejs, it does not work
what im i doing wrong or haven't done?
i can see the response from the node.js server and even save them in localstorage (like the token)
i can also retrieve the token from localstorage
setting the token from localstorage (method 2) or setting it direct from the response (method 1) is what is not happening
i appreciate your help
First of all, you shouldn't create authentication cookies or save any kind of authorization token this way. It makes a good vulnerability for XSS attacks. You should try to set the cookie from your backend with the httpOnly attribute which is not possible from the client side.
Regarding your problem, it's quite difficult to say what could be the problem on production. My best guess is that your production environment is using https and your cookie is being set insecurely by the package you are using as its the default. Therefor, it is only accessible when using http which you are probably using for development.
Try to set the config to use secure cookies when your import.meta.env.PROD equals true like this example below:
$cookies.config('35m', '', '', import.meta.env.PROD)
You should also make sure that the correct domain is set so it's accessible from the client.

How to access httpOnly cookies from Nuxt 3 server

I am implementing a login feature to a website project. The backend is Express and the frontend is Nuxt 3. Upon successfully authenticating a user login, the Express backend returns necessary data to the webserver, which then creates an httpOnly cookie and sets any necessary data in a Pinia store. On page refresh, I would like the Nuxt 3 server to look at the cookie and setup the Pinia store (since it is lost on page refresh).
Can someone provide some guidance? I have looked at the useNuxtApp() composable, and I can see the cookie in nuxtApp.ssrContext.req.headers.cookie, but that only provides a K/V pairing of all set cookies, which I would need to parse. I know of the useCookie composable, but that only works during Lifecycle hooks, which seems to only resolve undefined.
Thanks.
Not sure if this is the right way,
but it's a solution I used to get through a similar case - dotnet api + nuxt3 client.
First, we need to proxy API (express in your case),
this will make it, so our cookie is on the same domain and browser will start sending it to /api/ endpoints.
Install #nuxtjs-alt/proxy - npm i #nuxtjs-alt/proxy.
Add configuration to nuxt.config.ts (my api running on localhost:3000):
nuxt.config.ts:
export default defineNuxtConfig({
modules: [
'#nuxtjs-alt/proxy'
],
proxy: {
enableProxy: true,
fetch: true,
proxies: {
'/proxy': {
target: 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/proxy/, '')
}
}
}
});
Then we can the request that will set a cookie anywhere on client using proxy instead of a direct call.
Anywhere on client, do a request using newly setup proxy instead of calling API directly.
Adjust parameters based on your setup.
await $fetch('/proxy/user/sign-in', {
method: 'POST',
body: {
email: 'example#mail.com',
password: 'password'
}
});
Ultimately, should end up with a cookie set on our client domain.
And lastly, when we handle request client side - we read the cookie and set up on forwarding request.
Replace COOKIE_NAME and API URL accordingly.
server/api/user/me.get.ts:
export default defineEventHandler(async (event) => {
return await $fetch('http://localhost:3000/user/me', {
headers: {
Cookie: `COOKIE_NAME=${
getCookie(event, 'COOKIE_NAME')
}`
}
});
});
API call will use the same cookie we got when we did a first request using cookie and the server should be able to read it.

Cors is not working for two different origins

I have two servers, frontend (Next.js) and backend (express.js api server).
Frontend server is running without any additions. But I have an nginx proxy for backend.
So far everything is good because they are not connected yet.
Frontend: is working as it should be.
Backend: I can make calls directly from the backend itself (by self origin).
When I make a fetch get call from my frontend server to the backend server, it normally gives a cors error because the origins are different.
For this, I set the backend server with cors:
// /src/middlewares/cors.ts
import cors from 'cors';
const whitelist = new Set(['http://192.168.1.106:3000', 'https://192.168.148.132']);
// frontend: http://192.168.1.106:3000
// backend: https://192.168.148.132
const corsOptions = {
optionsSuccessStatus: 200,
origin: (origin: any, callback: any) => {
console.log('origin: ' + origin);
if (whitelist.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
// credentials: true,
};
export default cors(corsOptions);
and
// /app.ts
import cors from './middlewares/system/cors.js';
.
.
// setup cors
app.options('*', cors);
app.use(cors);
.
.
After doing this, I reach my main goal. The frontend server can make call to the backend server.
output:
But this time I notice a problem. I can't send self request to backend anymore (by self origin).
When dealing with this I looked at the origins that came to the /src/middlewares/cors.ts file that I showed above.
for frontend:
for backend:
I am using self signed ssl in nginx for back server.
And there is not any cors relevant headers in conf.
How can i solve this situation?
(If I'm missing something, you can point it out in the comments.)
The Origin header is only set in cross-origin requests. If you call your backend directly, the Javascript value is undefined, and in this case you must not restrict anything. You could, for example, write
if (!origin || whitelist.has(origin)) {
callback(null, true);
}

How to get JWT cookies in our react application, how to check the user is login or not I am unable to find how to handle my react application session

How to get JWT cookies in our react application, how to check the user is login or not I am unable to find how to handle my react application session.
I really appreciate who helps me out with this problem.
Thanks in advance
The server side API is setting the HTTPOnly cookie which you wont be able to read in JS.
What you need to do it in your react App handle a 401 status error and based on that set a flag isAuthenticated or something as false. Otherwise keep it to be true. With each request to the server HTTPOnly cookie would be sent automatically so you don't need to handle the token inside a cookie.
The backend code needs to send a 401 once the cookie is expired, or the logout is requested or the JWT inside a cookie expires.
Before I say anything, you have included app.use(cookieParser()) in index.js right? Because if not, you're gonna need that once you've installed it with npm i cookie-parser
But anyway, a few things:
You can create a PrivateRoute in React, as far as I'm aware this tends to work well to protect routes from unauthorized users.
You can simply store an isAuthenticated in either the state or localStorage: however this will require that you make absolutely sure that a user shouldn't be able to just change the value in the state or add isAuthenticated in localStorage and just spoof authenticity (this is the part that took me the longest to get right).
Anyway, the way I've handled this is that when a user logs in an access token (and a refresh token if it doesn't already exists) are generated from the server and sent to the client in an httpOnly cookie, while this makes it so that you can't do anything with it on the client side with JavaScript as Pavan pointed out in his answer (which is generally a good thing), you should use res.status for validation when you make the fetch request. Here's what my fetch request kind of looks like:
const login = async (user) => {
const body = JSON.stringify(user);
return fetch(loginURL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include', //this is important
body: body
}).then(function(res) {
if (res.status === 200) {
const id = Date.now();
localStorage.sid = id; //this is temporary
return res.json()
} else {
return res.json()
}
})
// you can ignore this part
.then(async resp => {
return resp
})
}
Side-note: Your browser automatically handles the httpOnly cookies you send from your server however the credentials: 'include' needs to be included in your subsequent fetch requests.
In one of my main parent components, the login function is called:
login = async (user) => {
this.setState({ error: null });
await adapter.login(user).then(data => {
if (!data.error) {
this.setState({session: "active"})
} else if (data.error && data.error.status === 401) {
// this is so I can handle displaying invalid credentials errors and stuff
this.setState({ error: data.error, loading: false });
}
});
}
I also have a middleware on the server-side that is run before any of the code in the routes to verify that the user making the request is actually authorized. This is also what handles access token expiration for me; if the access token has expired, I use the refresh token to generate a new access token and send it back in the httpOnly cookie, again with a status of 200 so the user experience isn't jarring. This does of course mean that your refresh token would have to live longer than your access token (I haven't decided how long in my case yet, but I'm thinking either 7 or 14 days) though as far as I'm aware that is okay to do.
One last thing, the parent component I referred to earlier calls a validate function which is a fetch request to the server within its componentDidMount so that the user is verified each time the component mounts, and then I've included some conditional rendering:
render() {
return (
<div className="container">
{
!localStorage.sid && <LoginForms {...yourPropsHere}/>
}
{
this.state.loading && <Loading />
}
{
localStorage.sid && this.state.session === "active" && <Route path="/" render={(props) => <Root error={this.state.error} {...props}/>}/>
}
</div>
);
}
I've gone the conditional rendering route as I couldn't get PrivateRoute to work properly in the time that I had, but either should be fine.
Hopefully this helps.
Your login API should return JWT token and how long it should be live.
Your login API response would be like :
{
jwt: your jwt token,
duration: in seconds
}
Use universal-cookies NPM to store this result into cookies.
For more details how to manipulate with cookies, visit
https://www.npmjs.com/package/universal-cookie
For setting cookies your code like:
const cookies = new Cookies();
cookies.set(name of cookies, jwt value from API call, {
maxAge: duration,
});
Above code store the jwt cookies in browser and after maxAge automatically remove it from browser.
So for identifying session is present or not, you should check after specific interval cookies has present in browser or not. If cookies has present in browser then session is on, otherwise session has expired.

HTTP headers in Websockets client API

Looks like it's easy to add custom HTTP headers to your websocket client with any HTTP header client which supports this, but I can't find how to do it with the web platform's WebSocket API.
Anyone has a clue on how to achieve it?
var ws = new WebSocket("ws://example.com/service");
Specifically, I need to be able to send an HTTP Authorization header.
Updated 2x
Short answer: No, only the path and protocol field can be specified.
Longer answer:
There is no method in the JavaScript WebSockets API for specifying additional headers for the client/browser to send. The HTTP path ("GET /xyz") and protocol header ("Sec-WebSocket-Protocol") can be specified in the WebSocket constructor.
The Sec-WebSocket-Protocol header (which is sometimes extended to be used in websocket specific authentication) is generated from the optional second argument to the WebSocket constructor:
var ws = new WebSocket("ws://example.com/path", "protocol");
var ws = new WebSocket("ws://example.com/path", ["protocol1", "protocol2"]);
The above results in the following headers:
Sec-WebSocket-Protocol: protocol
and
Sec-WebSocket-Protocol: protocol1, protocol2
A common pattern for achieving WebSocket authentication/authorization is to implement a ticketing system where the page hosting the WebSocket client requests a ticket from the server and then passes this ticket during WebSocket connection setup either in the URL/query string, in the protocol field, or required as the first message after the connection is established. The server then only allows the connection to continue if the ticket is valid (exists, has not been already used, client IP encoded in ticket matches, timestamp in ticket is recent, etc). Here is a summary of WebSocket security information: https://devcenter.heroku.com/articles/websocket-security
Basic authentication was formerly an option but this has been deprecated and modern browsers don't send the header even if it is specified.
Basic Auth Info (Deprecated - No longer functional):
NOTE: the following information is no longer accurate in any modern browsers.
The Authorization header is generated from the username and password (or just username) field of the WebSocket URI:
var ws = new WebSocket("ws://username:password#example.com")
The above results in the following header with the string "username:password" base64 encoded:
Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ=
I have tested basic auth in Chrome 55 and Firefox 50 and verified that the basic auth info is indeed negotiated with the server (this may not work in Safari).
Thanks to Dmitry Frank's for the basic auth answer
More of an alternate solution, but all modern browsers send the domain cookies along with the connection, so using:
var authToken = 'R3YKZFKBVi';
document.cookie = 'X-Authorization=' + authToken + '; path=/';
var ws = new WebSocket(
'wss://localhost:9000/wss/'
);
End up with the request connection headers:
Cookie: X-Authorization=R3YKZFKBVi
Sending Authorization header is not possible.
Attaching a token query parameter is an option. However, in some circumstances, it may be undesirable to send your main login token in plain text as a query parameter because it is more opaque than using a header and will end up being logged whoknowswhere. If this raises security concerns for you, an alternative is to use a secondary JWT token just for the web socket stuff.
Create a REST endpoint for generating this JWT, which can of course only be accessed by users authenticated with your primary login token (transmitted via header). The web socket JWT can be configured differently than your login token, e.g. with a shorter timeout, so it's safer to send around as query param of your upgrade request.
Create a separate JwtAuthHandler for the same route you register the SockJS eventbusHandler on. Make sure your auth handler is registered first, so you can check the web socket token against your database (the JWT should be somehow linked to your user in the backend).
HTTP Authorization header problem can be addressed with the following:
var ws = new WebSocket("ws://username:password#example.com/service");
Then, a proper Basic Authorization HTTP header will be set with the provided username and password. If you need Basic Authorization, then you're all set.
I want to use Bearer however, and I resorted to the following trick: I connect to the server as follows:
var ws = new WebSocket("ws://my_token#example.com/service");
And when my code at the server side receives Basic Authorization header with non-empty username and empty password, then it interprets the username as a token.
You cannot add headers but, if you just need to pass values to the server at the moment of the connection, you can specify a query string part on the url:
var ws = new WebSocket("ws://example.com/service?key1=value1&key2=value2");
That URL is valid but - of course - you'll need to modify your server code to parse it.
You can not send custom header when you want to establish WebSockets connection using JavaScript WebSockets API.
You can use Subprotocols headers by using the second WebSocket class constructor:
var ws = new WebSocket("ws://example.com/service", "soap");
and then you can get the Subprotocols headers using Sec-WebSocket-Protocol key on the server.
There is also a limitation, your Subprotocols headers values can not contain a comma (,) !
For those still struggling in 2021, Node JS global web sockets class has an additional options field in the constructor. if you go to the implementation of the the WebSockets class, you will find this variable declaration. You can see it accepts three params url, which is required, protocols(optional), which is either a string, an array of strings or null. Then a third param which is options. our interest, an object and (still optional). see ...
declare var WebSocket: {
prototype: WebSocket;
new (
uri: string,
protocols?: string | string[] | null,
options?: {
headers: { [headerName: string]: string };
[optionName: string]: any;
} | null,
): WebSocket;
readonly CLOSED: number;
readonly CLOSING: number;
readonly CONNECTING: number;
readonly OPEN: number;
};
If you are using a Node Js library like react , react-native. here is an example of how you can do it.
const ws = new WebSocket(WEB_SOCKETS_URL, null, {
headers: {
['Set-Cookie']: cookie,
},
});
Notice for the protocols I have passed null. If you are using jwt, you can pass the Authorisation header with Bearer + token
Disclaimer, this might not be supported by all browsers outside the box, from the MDN web docs you can see only two params are documented.
see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#syntax
Totally hacked it like this, thanks to kanaka's answer.
Client:
var ws = new WebSocket(
'ws://localhost:8080/connect/' + this.state.room.id,
store('token') || cookie('token')
);
Server (using Koa2 in this example, but should be similar wherever):
var url = ctx.websocket.upgradeReq.url; // can use to get url/query params
var authToken = ctx.websocket.upgradeReq.headers['sec-websocket-protocol'];
// Can then decode the auth token and do any session/user stuff...
In my situation (Azure Time Series Insights wss://)
Using the ReconnectingWebsocket wrapper and was able to achieve adding headers with a simple solution:
socket.onopen = function(e) {
socket.send(payload);
};
Where payload in this case is:
{
"headers": {
"Authorization": "Bearer TOKEN",
"x-ms-client-request-id": "CLIENT_ID"
},
"content": {
"searchSpan": {
"from": "UTCDATETIME",
"to": "UTCDATETIME"
},
"top": {
"sort": [
{
"input": {"builtInProperty": "$ts"},
"order": "Asc"
}],
"count": 1000
}}}
to all future debugger - until today i.e 15-07-21
Browser also don't support sending customer headers to the server, so any such code
import * as sock from 'websocket'
const headers = {
Authorization: "bearer " + token
};
console.log(headers);
const wsclient = new sock.w3cwebsocket(
'wss://' + 'myserver.com' + '/api/ws',
'',
'',
headers,
null
);
This is not going to work in browser. The reason behind that is browser native Websocket constructor does not accept headers.
You can easily get misguided because w3cwebsocket contractor accepts headers as i have shown above. This works in node.js however.
The recommended way to do this is through URL query parameters
// authorization: Basic abc123
// content-type: application/json
let ws = new WebSocket(
"ws://example.com/service?authorization=basic%20abc123&content-type=application%2Fjson"
);
This is considered a safe best-practice because:
Headers aren't supported by WebSockets
Headers are advised against during the HTTP -> WebSocket upgrade because CORS is not enforced
SSL encrypts query paramaters
Browsers don't cache WebSocket connections the same way they do with URLs
What I have found works best is to send your jwt to the server just like a regular message. Have the server listening for this message and verify at that point. If valid add it to your stored list of connections. Otherwise send back a message saying it was invalid and close the connection. Here is the client side code. For context the backend is a nestjs server using Websockets.
socket.send(
JSON.stringify({
event: 'auth',
data: jwt
})
);
My case:
I want to connect to a production WS server a www.mycompany.com/api/ws...
using real credentials (a session cookie)...
from a local page (localhost:8000).
Setting document.cookie = "sessionid=foobar;path=/" won't help as domains don't match.
The solution:
Add 127.0.0.1 wsdev.company.com to /etc/hosts.
This way your browser will use cookies from mycompany.com when connecting to www.mycompany.com/api/ws as you are connecting from a valid subdomain wsdev.company.com.
You can pass the headers as a key-value in the third parameter (options) inside an object.
Example with Authorization token. Left the protocol (second parameter) as null
ws = new WebSocket(‘ws://localhost’, null, { headers: { Authorization: token }})
Edit: Seems that this approach only works with nodejs library not with standard browser implementation. Leaving it because it might be useful to some people.
Technically, you will be sending these headers through the connect function before the protocol upgrade phase. This worked for me in a nodejs project:
var WebSocketClient = require('websocket').client;
var ws = new WebSocketClient();
ws.connect(url, '', headers);

Categories

Resources