In dev environment, whenever client get cookies, client will stored it until exprires out date. But when i deploy server on Heroku, and client on Vercel then it not work.I tried to set
app.set("trust proxy", 1);
or change sameSite from "lax" to "none" but it still not work.
At server, i create a fuction to set cookie
export const sendRefreshToken = (res: Response, userId: string) => {
res.cookie(
process.env.REFRESH_TOKEN_COOKIE_NAME!,
createToken("refreshToken", userId),
{
httpOnly: true,
secure: true,
sameSite: __prod__ ? "lax" : "none",
path: "/refresh_token",
expires: new Date(Date.now() + 86400 * 1000 * 180), //180days
}
);
};
at client, credentials of Apollo Client is true
const httpLink = new HttpLink({
uri:
process.env.NODE_ENV === "production"
? "link-web"
: "http://localhost:4000/graphql",
credentials: "include",
});
const authMiddleware = new ApolloLink((operation, forward) => {
const token = JwtManager.getToken();
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
},
}));
return forward(operation);
});
export const client = new ApolloClient({
cache: new InMemoryCache()
link: concat(authMiddleware, httpLink),
});
I can see the cookie at client whenever server return cookie
Access-Control-Allow-Credentials
true
Access-Control-Allow-Origin
https://test-deploy-zeta-two.vercel.app
Connection
keep-alive
Server : Cowboy
Set-Cookie : myCookie=myValue; Path=/refresh_token; Expires=Tue, 25 Oct 2022 05:52:49 GMT; HttpOnly; Secure; SameSite=None
X-Powered-By : Express
...
But when i reload the page,this cookie disappear.How can i fix it?
Related
Background:
I use express-session to do authetication in my MERN project.
After I logged in, a session will be created and sessionId will be send to client side by cookie. So when I am trying to fetch data, server will verify my sessionId.
I run my project in dev-env, and everything goes well. I can get the cookies in chrome devtools.
BUT after I deployed my project on Vercel(Server and Client depolyed separately). When I login in my web, there are no cookies in devtools.Verification failed, no sessionId returned.
Below are my codes:
server side:
//setup express-session
app.use(
session({
secret: process.env.SESSION_SECRET,
store: store,
resave: false,
saveUninitialized: false,
cookie: {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "PRODUCTION" ? true : false,
maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week
},
name: "rd_sid",
})
);
//setup cors
const corsOptions = {
origin: process.env.CORS_ORIGIN.split(",").map(
(origin) => new RegExp(origin)
),
allowedHeaders: "Origin, X-Requested-With, Content-Type, Accept",
credentials: true,
};
app.use(cors(corsOptions));
client side:
export default function ajax(url, data = {}, method = "GET") {
return new Promise((resolve, isRejected) => {
let promise;
// 1.1 Execute ajax request
if (method === "GET") {
promise = axios.get(url, {
params: data,
withCredentials: true,
});
} else if (method === "POST") {
promise = axios.post(url, data, { withCredentials: true });
}
promise
.then((response) => {
// 1.2 If success, call resolve(value)
resolve(response.data);
})
.catch((error) => {
// 1.3 If fail, do not call reject(reason) but alert error message.
message.error("request error:" + error.message);
});
});
}
I set withCredentials to ture, and also set the header for cors. So it will solve the cross domain problem.
I try to switch secure , httpOnly, and change the cookie's name. But they seem not work.
I'm making an API in Nestjs that is consumed by an application in ReactJs. My problem is in the login route, when I use swagger, the cookie is saved in the browser normally but when I do a fetch from the front end, the cookie is not saved even though the response headers have the cookie.
I already tried to use all the sameSite options, I tried to put credentials include in the fetch but nothing works. If I log in to swagger first, then I try to do it in react, react copies the cookie that is saved in swagger.
For example, if in swagger I log in with user 1, and in react with the user 2, react steals the cookie from user 1 and ignores user 2 response cookie.
Code in react:
const res = await fetch(`${API_URL}/auth/login`, {
method: "POST",
headers: { "Content-type": "application/json", accept: "*/*" },
// credentials: "include",
body: JSON.stringify(data),
});
Main.ts:
const corsOptions = {
origin:
process.env.NODE_ENV === 'development' ||
process.env.MY_NODE_ENV === 'development'
? [process.env.PLATFORM_LOCAL_URL, process.env.LANDING_LOCAL_URL]
: [process.env.PLATFORM_PROD_URL, process.env.LANDING_PROD_URL],
credentials: true,
allowedHeaders: 'Content-Type, Accept, Origin',
preflightContinue: false,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
};
app.enableCors(corsOptions);
app.use(helmet());
app.use(cookieParser());
Login Controller:
#UseGuards(LocalAuthGuard)
#Post('auth/login')
async login(
#Body() _: MakeAuthDto,
#Request() req,
#Res({ passthrough: true }) res,
) {
const access_token = await this.authService.login(req.user);
const cookiesOpts = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'none',
path: '/',
maxAge: 60 * 60 * 24 * 3,
};
res.cookie('jwt', access_token, cookiesOpts);
return {
response: {
user: req.user,
expire: new Date().setDate(new Date().getDate() + 3),
},
};
}
Work on swagger:
After make request from ReactJs, the response cookies has the jwt:
But the cookie are not stored:
Looks like you're trying to set a cookie with the swagger editor.
See Note for Swagger UI and Swagger Editor users:
Cookie authentication is currently not supported for "try it out" requests due to browser security restrictions. See this issue for more information. SwaggerHub does not have this limitation.
tl:dr;
A Node (express) server is hosted on Heroku, and the UI is hosted on Netlify. When the UI makes a REST API call to the server, the session doesn't persist (but it persists if I ran both locally. localhost:5000 on the server, localhost:3000 on UI. The UI is proxying requests with package.json).
Code snippets
session.ts
export const sessionConfig = {
secret: process.env.SESSION_KEY,
store: new RedisStore({ client: redisClient }),
resave: true,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax',
},
};
server.ts
const app = express();
app.use(express.json());
app.use(cookieParser());
app.set('trust proxy', 1);
app.use(session(sessionConfig)); // This sessionConfig comes from the file above
app.use(cors({
credentials: true,
origin: process.env.CLIENT_URL,
}));
I googled something like express session not persist when cross domain request. Then, I saw threads like this and this. It seems that app.set('trust proxy', 1) will make sure that session data will be persisted for cross-domain requests. Apparently, in my case, something is still missing.
Does anyone see what I'm doing wrong? Any advice will be appreciated!
PS:
I'm using sessions for captcha tests, which looks like...
captch.ts
CaptchaRouter.get('/api/captcha', async (req: Request, res: Response) => {
const captcha = CaptchaService.createCaptcha();
req.session.captchaText = captcha.text;
res.send(captcha.data);
});
CaptchaRouter.post('/api/captcha', async (req: Request, res: Response) => {
if (req.session.captchaText !== req.body.captchaText) {
throw new BadRequestError('Wrong code was provided');
}
// The client sent the correct captcha
},
);
Another PS:
Here's how the response heders look like:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.netlify.app
Connection: keep-alive
Content-Length: 46
Content-Type: application/json; charset=utf-8
Date: Sun, 09 Jan 2022 00:00:00 GMT
Etag: W/"2e-cds5jiaerjikllkslaxmalmird"
Server: Cowboy
Set-Cookie: connect.sid=s%3ramdon-string-here; Path=/; Expires=Sun, 09 Jan 2022 00:00:00 GMT; HttpOnly; Secure; SameSite=None
Vary: Origin
Via: 1.1 vegur
X-Powered-By: Express
The cause was that the client (hosted on Netlify) wasn't proxying API requests.
The solution was:
add _redirects under public of the client
/api/* https://server.herokuapp.com/api/:splat 200
/* /index.html 200
make sure that API requests from the client will begin with the root URL
return axios({ method: 'POST', url: '/api/example', headers: defaultHeaders });
For future reference, here's my session config
const sessionConfig = {
secret: process.env.SESSION_KEY || 'This fallback string is necessary for Typescript',
store: new RedisStore({ client: redisClient }),
resave: false,
saveUninitialized: true,
cookie: {
secure: process.env.NODE_ENV === 'production', // Prod is supposed to use https
sameSite: process.env.NODE_ENV === "production" ? 'none' : 'lax', // must be 'none' to enable cross-site delivery
httpOnly: true,
maxAge: 1000 * 60
} as { secure: boolean },
};
...and here's server.ts
const app = express();
const port = process.env.PORT || 5000;
app.use(express.json());
app.set('trust proxy', 1);
app.use(session(sessionConfig));
app.use(cors({
credentials: true,
origin: process.env.CLIENT_URL,
}));
(As #Matt Davis pointed out, cookieParser was unnecessary)
PS
I haven't tried to set cookie.domain in the session config. If this was set to the client URL (provided by Netlify), did the session cookie persist?
In my local environment, I'm trying to set up user tokens after login in my NextJS app (localhost:3005), using the response from my express backend (localhost:3020). I can see set-cookie in the response on the server, but the cookies in the getServerSideProps is empty always.
My code is pretty basic:
Express backend
// Cors set up
cors({
methods: "GET,HEAD,OPTIONS,POST",
preflightContinue: false,
credentials: true,
origin: [
"http://localhost:3005",
...
],
optionsSuccessStatus: 204,
})
// Response - can see this cookie in set-cookie
return res.cookie("test", "test1", {
httpOnly: true,
sameSite: "none",
expires,
domain: "localhost:3005", // tried without this also
}).redirect("http://localhost:3005/login");
My NextJS app has the following:
//Login component : on login submit
const resp = await fetch(
`${BACKEND_URL}login?originalUrl=/login`, // Redirecting back
{
headers: new Headers({
Authorization: "Bearer " + someToken,
}),
credentials: "include",
method: "POST",
redirect: "follow", // Tried this but did not work
}
);
// resp.redirected is true
// Login component - triggers correctly but no cookies
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { req } = ctx;
const { cookies } = req;
console.log("cookies", cookies ); // Always {}
return { props: {} };
};
I have been stuck with this for a while now guys, it seems like I'm missing something very trivial here. I'm not super familiar with NextJs, so any help here would be greatly appreciated. Thanks!
This was happening because of the Chrome SameSite=None + Secure requirement. Setting secure to true in the cookie resolved this issue. I did not have to set up SSL certificates for my local environment, just setting secure worked.
I work with app, that already has its own infrastructure. The task is to prevent user login in several browser. Our application has single app architecture, so ideally user should work only in one browser tab. And I have a problem. I can’t remove cookie from client.
I. Briefly.
App settings:
Server: NodeJS
Port: 8083
Client: VueJS
Port: 8088
I use module express-session to initialize session mechanism on server side and send cookies to client. Client hasn’t set cookies.
II. Details:
Server’s root file is index.js
I do the following in it:
Plug in express module:
const express = require('express')
Plug in cors module:
const cors = require('cors')
Add cors settings:
app.use(cors({
origin: 'http://localhost:8088',
credentials: true
}))
Then I initialize session in user.js file and receive client’s connects:
Plug in express-session module:
const session = require('express-session')
Plug in routing by express.Router():
const router = express.Router()
Add session settings:
const EIGHT_HOURS = 1000 * 60 * 60 * 8
const {
SESS_NAME = 'sid',
SESS_LIFETIME = EIGHT_HOURS,
SESS_SECRET = 'test',
NODE_ENV = 'development'
} = process.env
const IN_PROD = NODE_ENV === 'production'
Initialize session:
router.use(session({
name: SESS_NAME,
resave: false,
saveUninitialized: false,
secret: SESS_SECRET,
cookie: {
maxAge: SESS_LIFETIME,
sameSite: false,
// Must have HTTPS to work 'secret:true'
secure: IN_PROD
}
}))
Receive client queries by router.post()
So what I did:
I use req.session.destroy to remove session data and expect the browser logout user from certain browser and cookies clear.
req.session.destroy(err => {
if (err) {
return res.send({ error: 'Logout error' })
}
res.clearCookie(SESS_NAME, {path: '/'})
return res.send({ 'clearSession': 'success' })
})
Unfortunately nothing magic happens
I read different topics. For example, here (GitHub) offer the conclusion: use explicit cookie’s path indication in res.clearCookie method as shown above.
That didn’t work.
Wrote this setting {path: '/'} in cookies settings. Didn’t work too.
router.use(session({
name: SESS_NAME,
resave: false,
saveUninitialized: false,
secret: SESS_SECRET,
cookie: {
path: '/',
maxAge: SESS_LIFETIME,
sameSite: false,
// Must have HTTPS to work 'secret:true'
secure: IN_PROD
}
}))
And as wrote in express-session documentation (NPM:express-session) this path is the default path for cookie storage.
Add req.session = null in req.session.destroy:
req.session.destroy(err => {
if (err) {
return res.send({ error: 'Logout error' })
}
req.session = null
res.clearCookie(SESS_NAME, {path: '/'})
return res.send({ 'clearSession': 'success' })
})
That didn’t work
delete req.session doesn’t work too.
So, how can I resolve this problem? What should I do?
adding .send('cleared cookie') made my browser clear its cache of the named cookie.
const logOutRequest = (req, res) => {
req.session.destroy((err) => {
res.clearCookie("notcookie").send('cleared cookie');
});
};
Have you tried removing the exact cookie by setting it to null that is lets say that you are dealing with a cookie named Views you could remove the cookie using req.session.Views = null
Instead of doing this
req.session.destroy(err => {
if (err) {
return res.send({ error: 'Logout error' })
}
req.session = null
res.clearCookie(SESS_NAME, {path: '/'})
return res.send({ 'clearSession': 'success' })
})
you could do the name of your session cookie and set that to null that is
req.session.sid= null
This removes the cookie from client browser
req.session.destroy(err => {
res.clearCookie("session-cookie-name", { path: "/" });
});
The most important key is setting 'domain' in the clearCookie method to solve your issue. Expressjs will return the following in the http response header. However, it seems that on the browser or some browsers that I tested, it doesn't know which cookie belonging to which domain to clear, hence, the cookie remains there. You do not need to include the path when calling clearCookie
Set-Cookie: mycookie=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
You have to set domain like below:
req.session.destroy(err => {
res.clearCookie("session-cookie-name", { domain: 'your-domain' });
});
Then response header will become
Set-Cookie: mycookie=; Domain=your-domain; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT
The browser will clear the cookie nicely!
this works for me
#Post('signout')
async signout(#Req() req: Request, #Res({ passthrough: true }) res: Response) {
const user = req.user
if (!user) return {}
await new Promise<void>((resolve, reject) => {
req.session.destroy((err) => {
if (err) {
reject(err)
} else {
res.clearCookie('ACCESS_TOKEN', {
domain: '.xxx.com'
})
res.clearCookie('REFRESH_TOKEN', {
domain: '.xxx.com'
})
res.clearCookie('connect.sid', {
domain: '.xxx.com'
})
resolve()
}
})
})
return {}
}