We have a NodeJS express server with express-session and express-mysql-session enabled. We've connected the session database and can verify there are sessions being created. Our app redirects users without a token cookie to Auth0 for authentication, and they are redirected back to our app at /callback. It does successfully set our ID token that allows our user to be logged in. However, the passport auth0 strategy complains that it cannot verify the authentication state.
'Unable to verify authorization request state.'
The server code is below, with some other parts commented out. Connecting to the session database is successful since we can see our sessions get created with each request.
const options = {
database: process.env.SESSION_DB_NAME || "sessions",
host: process.env.SESSION_DB_HOST! as string,
password: process.env.SESSION_DB_PASSWORD! as string,
port: parseInt(process.env.SESSION_DB_PASSWORD || "3306", 10),
user: process.env.SESSION_DB_USER! as string,
};
const sessionStore = new MySQLStore(options);
app.use(
session({
cookie: {
httpOnly: true,
maxAge: 24 * 3600 * 1000, // 1 day
secure: true,
},
resave: false,
saveUninitialized: true,
secret: process.env.SESSION_SECRET!,
store: sessionStore,
})
);
app.get("/login", (request, response, next) => {
// if there is no session, it's a server error
if (!request.session) {
console.error(new Error("No session in /login"));
return response.status(500).send("Server error: No Session");
}
return passport.authenticate("auth0", {
scope: "openid email profile",
})(request, response, next);
});
console.log("router: Auth0 Callback");
app.get("/callback", (request, response, next) => {
const domain = request.hostname;
console.log(request.session);
passport.authenticate("auth0", (auth0Error, token, info) => {
// if there was a problem authenticating
if (auth0Error) {
console.error(auth0Error);
return response.status(500).send();
}
// ensure we have a session. The middleware should be giving us one.
if (!request.session) {
console.error("No session in /callback.");
return response.status(500).send();
}
// if we couldn't log in
if (!token) {
if (info) {
console.log(info);
return response.status(500).send(info.message);
}
// redirect to the login page
return response.status(401).send("Cannot log you in.");
}
// parse the token we get
const user = jwt.decode(token);
// if there is no valid user token, we've got bigger problems
if (!user) {
console.error("Token is falsey after authenticating.");
return response.status(500).send();
} else if (typeof user === "string") {
console.error("Cannot decode JWT after authenticating");
return response.status(500).send();
}
request.logIn(user, (logInError) => {
// redirect to desired route
// ...
});
})(request, response, next);
});
router.get("/*", (request, response, next) => {
passport.authenticate("jwt", (authenticationError, isAuthenticated, info) => {
// Log in successful via existing JWT
if (isAuthenticated) {
return next();
}
// handle unauthenticated situations
// ...
})(request, response, next);
});
I've tried the following suggestions at https://github.com/auth0/passport-auth0/issues/70:
app.set("trust proxy", 1")
session cookie already set to secure
serving only on https
using mysql session store and default in-memory store
We are running the server in a kubernetes cluster using ClusterIP for our ingress. Env variables are being set correctly, I'm pretty sure. When I run this locally I do not have any issues.
What could cause the session to not be restored but instead recreated with each request?
Your question was posted over a year ago so I figure you've already fixed it, but I thought I'd post my solution to help others. I very recently ran into the same issue, although I was using an Apache reverse proxy instead of a Kubernetes cluster.
I've posted my solution in another SO question: https://stackoverflow.com/a/67891167/8221175
In short, my Apache configuration was missing these lines:
RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
RequestHeader set X-Forwarded-SSL expr=%{HTTPS}
ProxyPass / http://localhost:8001/
ProxyPassReverse / http://localhost:8001/
This, along with other session/cookie configuration for express (see the SO link) fixed it. My sessions now persist and I no longer have the issue.
For those interested in why the 'Unable to verify authorization request state.' message is issued, check out my answer in this GitHub issue: https://github.com/auth0/passport-auth0/issues/89#issuecomment-856971807
Related
Vite + React: http://localhost:5173
Express: http://localhost:3000
Here is the code in the express server's login route:
const mycookie = cookie.serialize("jwt", refreshToken, {
httpOnly: true, // Set the HTTP-only flag
secure: true, // Set the secure flag
sameSite: "none",
path: "/", // Set the path of the cookie to '/'
maxAge: 3600, // Set the maximum age of the cookie to 1 hour
});
// Set the cookie in the response headers
res.setHeader("Set-Cookie", mycookie);
res.json({
accessToken,
refreshToken,
});
Here is my cors config:
const allowedOrigins = require("./allowedOrigins");
const corsOptions = {
crendials: true,
origin: function (origin, callback) {
console.log(allowedOrigins.indexOf(origin));
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
};
Here is my handleSubmit function in the frontend (gets called when the user clicks submit):
const handleSubmit = () => {
console.log("Login");
// axios.defaults.withCredentials = true;
axios.post("http://localhost:3000/api/v1/auth/login", { email, password });
};
Whenever the request is made to the server. The response header does contain the set-Header to set the jwt token but I am not able to see it in my applications tab under cookies in devtools. A pre-flight request also comes in which probably clears the cookie.
My networks tab:
The xhr request:
The OPTIONS request:
The Applications tab:
However, when I disable CORS in the browser, the cookie is getting set.
Networks tab (NOTICE: No PreFlight request)
Applications tab:
I tried working out different answers from stackoverflow answers like this, this, this, and many more along with a reddit post on a similar issue but nothing has worked.
P.S. : I already tried using credentials: true
I have using express-session and connect-mongo for storing the session and it is working fine in the development environment. However, it is not persisting sessions in production. When I logged in I can get the session on the server by console.log(req.sesssion). However, the session is undefined if I move to another url.
const whitelist = ['https://example.com', 'https://www.example.com'];
const corsOptions = {
origin(origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
};
app.use(cors(corsOptions));
app.use(session({
secret: 'keyboard cat',
resave: false,
saveUninitialized: true,
store,
}));
After searching for the answer I have spent about a day. Finally, I found the issues causing all the headaches. The issue was related to http requests. I am using axios for the requests and by default, Axios does not fetch send or receive any cookies. To be able to work cookies I needed to pass Axios config options.{withCredentials: true} for every requests that I made. Thanks to the post author.
Axios.post(url, data, { withCredentials: true })...
However, that lead me to another issue.
"The value of the 'Access-Control-Allow-Credentials' header in the
response is '' which must be 'true' when the request's credentials
mode is 'include'."
Then I have added credentials: true to my cors() module like so.
const corsOptions = {
origin(origin, callback) {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
};
Now My project is live and serving users to make their lives easier :).
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 {}
}
I'm facing an issue trying to implement HTTPS via a proxy (initially NGINX, now AWS ALB) to secure connections to my node server. My login request is being processed just fine, but subsequent requests after login are being marked as isAuthenticated === false, and therefore my node app is returning 401.
I know isAuthenticated === false is being caused by the session cookie not being set in the browser, however I can't seem to figure out why the session cookie isn't being set (or sent?).
I'm configuring express-session as below:
app.use(cookieParser('secret'));
app.set('trust proxy', 1);
// add & configure middleware
app.use(session({
name: 'session',
store: new redisStore(),
secret: 'secret',
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true,
secure: true }
}));
My node app is sitting behind an AWS Application Load Balancer which communicates with the app over HTTP, so I've configured trust proxy to 1.
My passport configuration and local strategy are just below that:
// configure passport.js to use the local strategy
passport.use(new LocalStrategy(
{ usernameField: 'email' },
(email, password, done) => {
mysql.getConnection((err, conn) => {
conn.query('SELECT u.user_id, u.email, u.password, o.uuid FROM user AS u INNER JOIN organization AS o ON o.id = u.org_id WHERE email = ?;', [email], ( err, rows ) => {
if (err) throw err;
if (rows.length !== 1)
return done(null, false, { message: 'User does not exist.\n' });
else if (!bcrypt.compareSync(password, rows[0].password))
return done(null, false, { message: 'Incorrect password.\n' });
else{
return done(null, {user_id: rows[0].user_id, email: email, uuid: rows[0].uuid});
}
});
conn.release();
});
}
));
The request coming from the react client is:
axios.post('https://sub.mydomain.com' + '/api/login', userObj, {withCredentials: true})
.then(res => {
if (res.status === 200) {
initUser(res.data, true);
}
else {
this.setState({errors: {password: 'Incorrect username or password'}})
this.props.history.push('/login');
}
})
.catch((e) => {
this.setState({errors: {password: 'Unable to login'}})
this.props.history.push('/login');
});
When making the request I'm getting returned a 200 status with the user information as I would expect. See the screenshots from PostMan below:
Also these are the response headers in Chrome:
access-control-allow-credentials: true
access-control-allow-origin: https://sub.mydomain.com
content-length: 89
content-type: text/html; charset=utf-8
date: Tue, 12 Mar 2019 07:49:45 GMT
etag: W/"59-T0xi+VpB6A/MLCHMb8Qz3Pq9dwc"
status: 200
vary: Origin
x-powered-by: Express
Somewhere along the line it seems the session cookie is either failing to be sent from the node app, or set in the browser.
I'm running the exact same on my local machine and the cookie is being set in the browser no problem. The only difference between my local and my server is the ALB in front of the node application, and I'm setting secure: false on my local (as I'm not using HTTPS).
FYI - I had initially tried using an NGINX proxy_pass on my server to handle the HTTPS connections and had the same issue. I had tried removing the proxy pass and using the AWS Application Load Balancer after failing to find a solution with NGINX (I thought there may be an issue with my NGINX config). This leads me to believe the issue is with the express-session configuration as its the common denominator here, however I may be missing something else.
FYI 2 - I've tried setting trust proxy to true, 1, 2, 3, etc, Also tried setting secure to false, Also tried setting httpOnly to false - and countless variations of the above, but still I don't seem to be getting the cookie in the browser.
FYI 3 - I tried removing the proxy and reverting back to a direct HTTP connection to the Node application and I'm still not getting the cookie on in my browser (or in PostMan).
Hoping someone here can point out something I've missed in the hopes of getting this resolved.
Thanks in advance!
So after days of investigating, the issue turned out to be that my Redis server had terminated and failed to restart. facepalm
The sessionID wasn't being generated (or stored) as it couldn't connect to the Redis store to save the session information on the server side.
Starting the Redis server solved the issue immediately, however it would be great if express/passport had some verbose logging to notify users of this.
Hope someone else finds this useful!
I use React as a client to send request to Express with proxy and express-session set up. But every time React makes a request to Express server, a new session is created. So I checked Express alone by manually accessing to the same api url and it keep using the same session each time I refresh the page.
Project structure:
project-folder
- client // React client with proxy set up
+ src
+ package.json
+ ...
- server.js
- package.json
Inside server.js:
const session = require('express-session');
let sessionConf = {
name: 'aoid',
secret: 'stackoverflow',
resave: true,
saveUninitialized: true,
rolling: true,
cookie: {
httpOnly: false,
secure: false,
maxAge: 2000000
}
};
app.use(session(sessionConf));
app.get('/api/prod', (req, res, next) => {
let sessionId = req.sessionID; // is generated each time React client send request, works fine with api alone!
console.log(sessionId);
if (!sessionId) return res.status(401).send('Unauthorized Error');
res.status(200).send({ data });
});
Here is how React client send its request to Express:
let loadItems = async () => {
const response = await fetch('/api/prod');
const body = await response.json();
if (response.status !== 200) throw Error(body.message);
return body;
}
I think the problem comes from the misconfiguration between React and Express. Did anyone have this problem before?
fetch does not send cookie by default, you need to set it explicitly:
fetch(url, {
method: 'GET',
credentials: 'include',
// ...
})