Given the following resolver for a mutation:
async signin(parent, { name, password }, ctx, info) {
// Check if there is a user with the name
const user = await ctx.db.query.user({
where: { name }
})
if (!user) {
throw new Error('Name not found');
}
// Check if the password is correct
const valid = await bcrypt.compare(password, user.password);
if (!valid) {
throw new Error('Invalid password');
}
// Genereate the JWT
const token = jwt.sign({ userId: user.id }, process.env.APP_SECRET);
// Set the cookie with the token
ctx.response.cookie('token', token, {
httpOnly: true,
// secure: ...,
// sameSite: ...,
maxAge: 1000 * 60 * 60 * 24 * 365
});
return user;
}
When the code is like this, I get the following warning:
A cookie associated with a cross-site resource at http://timetable-yoga-pd.herokuapp.com/ was set without the SameSite attribute. It has been blocked, as Chrome now only delivers cookies with cross-site requests if they are set with SameSite=None and Secure. You can review cookies in developer tools under Application>Storage>Cookies and see more details at https://www.chromestatus.com/feature/5088147346030592 and https://www.chromestatus.com/feature/5633521622188032.
Backstory: the frontend part is a React app, that is hosted on timetable-react-pd.herokuapp.com. The backend part is a node app, hosted on timetable-yoga-pd.herokuapp.com.
Now, when I set the secure to true, as asked in the warning, the cookie header is not set. I have found on the internet, that this is normal.
When I set sameSite to false, it acts as if it were not there (expected behaviour). If sameSite is set to "none" or "None", the warning disappears, but a new error emerges, saying that GraphQL does not accept the value "none".
Related
My app consists of API (nestjs) and client (nextjs).
I am using getServerSideProps to authenticate the user, and redirect to login site if error occurs:
export const getServerSideProps: GetServerSideProps = async (context) => {
try {
// Check whether the user is logged in
await axios.get(
`${process.env.NEXT_PUBLIC_API}/authentication`,
{
withCredentials: true,
// Cookies are attached to the request with context
headers: context.req ? { cookie: context.req.headers?.cookie || '' } : { cookie: '' },
},
);
return {
props: {},
};
} catch (err) {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
};
Server side code for setting cookies:
public getCookieWithJwtToken(userId: number) {
const payload: TokenPayload = { userId };
const token = this.jwtService.sign(payload);
return `Authentication=${token}; HttpOnly; Path=/; Secure; SameSite=None; Max-Age=${this.configService.get(
'JWT_EXPIRATION_TIME',
)}`;
}
Cookies are working fine in development mode. I am able to log in, use the utilities for authenticated users and log out. However, when I deploy the server to vercel, the cookies are not persisting in the Application --> Cookies tab.
Cookies are being shown in cookie response, but they are not being applied into the browser.
Cookie information in production:
Cookies work as intended in development:
I've been struggling with this issue for several hours this far, help would be really appreciated.
so Ive finally deployed my app and resolved all the CORS issues, but I have a problem with user authentification. I can log-in, but when I refresh a site I get automaticly logged out -> on all browsers beside Mozilla Firefox, there it works somehow.
userContext.js -> Front-end
//XXX Login
const login = () => {
Axios.post(`${apiUrl}/users/login`, {
userName: nameLog,
userPassword: passwordLog,
}).then((response) => {
console.log(response);
if (!response.data.errors) {
setUser(response.data.name);
setUserId(response.data.user_id);
} else {
console.log(response);
const errors = response.data.errors;
console.log(errors);
processErrors(errors);
}
});
};
//Checking if user is logged in on every refresh of a page
useEffect(() => {
Axios.get(`${apiUrl}/users/login`).then((response, err) => {
console.log("GET /login RESPONSE: ", response);
if (response.data.loggedIn === true) {
setUser(response.data.user[0].name);
setUserId(response.data.user[0].user_id);
}
});
}, []);
First call is a POST request, thats when user logs in using form on my site.
And second one is a GET request, that checks if the session returns loggedIn true, this is called on every refresh of a page as it is inside useEffect hook.
Then I update my userState which acts as auth if user is allowed to do some action or not.
userRoutes.js -> Back-end
//Login user
router.post(
"/login",
[
check("userName").exists().notEmpty().withMessage("Username is empty.").isAlpha().isLength({ min: 3, max: 40 }),
check("userPassword").exists().notEmpty().withMessage("Password is empty.").isLength({ min: 3, max: 60 }).escape(),
],
(req, res) => {
const valErr = validationResult(req);
if (!valErr.isEmpty()) {
console.log(valErr);
return res.send(valErr);
}
const name = req.body.userName;
const password = req.body.userPassword;
const sql = "SELECT * FROM users WHERE name = ?";
db.query(sql, name, (err, result) => {
if (err) throw err;
if (!result.length > 0) {
res.send({ errors: [{ msg: "User doesn't exist" }] });
} else {
//compare hashed password from front end with hashed password from DB
bcrypt.compare(password, result[0].password, (error, match) => {
if (error) throw error;
//if passwords match send -> create session and send user data
if (match) {
req.session.user = result;
res.send({ user_id: result[0].user_id, name: result[0].name });
} else {
res.send({ errors: [{ msg: "Wrong username or password" }] });
}
});
}
});
}
);
//Checking if user is logged in and if so, sending user's data to front-end in session
router.get("/login", (req, res) => {
console.log("GET /login SESSION: ", req.session);
if (req.session.user) {
res.send({ loggedIn: true, user: req.session.user });
} else {
res.send({ loggedIn: false });
}
});
Again first one is for the POST request, where I create session and send it in response filled with user's data (name,id) to front-end (then I update my states accordingly).
Second one belongs to the GET request and returns false if user is not logged in or true + user's data. Then once again I update my states.
However this doesnt work and I dont know why. As I mentioned it returns loggedIn: false on every browser besides Mozzilla Firefox.
This is my first time dealing with sessions and cookies so what am I missing here?
By the way the site url is here if that helps: LINK, I left some console.logs() to display responses.
EDIT: adding all middleware
app.js -> main nodejs file
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const cors = require("cors");
const { check, validationResult } = require("express-validator");
const userRoutes = require("./routes/userRoutes.js");
const app = express();
app.use(express.json());
app.use(
cors({
origin: [
"http://localhost:3000",
"https://todo-react-node.netlify.app",
],
methods: ["GET, POST, PATCH, DELETE"],
credentials: true, //allowing cookies
})
);
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
console.log("SESSION 2 : ", req.session);
console.log("Cookies 2 : ", req.cookies);
next();
});
app.use(
session({
key: "userID",
secret: "subscribe",
resave: false,
saveUninitialized: false,
})
);
app.use("/users", userRoutes);
Your useEffect only called once if you are using [] in useEffect function.
Try to take it out(or add the suitable dependencies) and add a console.log() in that function to test out.
Furthermore, read the following article to gain some insight about useEffect.
Okay so finally Ive managed to solve this issue with sessions and cookies. Solution at the end.
What was causing problems?
Cross domain cookies (probably). As Ive mentioned everything worked fine when Ive hosted my app on localhost, but as soon as Ive deployed my app to 2 different hostings (Netlify - front end, Heroku - back end) my cookies werent coming trough.
Ive actually managed to send cookie by
res.cookie("cookie_name", "cookie_value", {sameSite: "none" secure: true})
, however It was standalone cookie and not associated with a session.
Sure why dont you edit your settings in express-session then? Ive tried -> didnt work. My session was never created. I could only send standalone cookie.
And another problem I couldnt delete this cookie in browser after being set, nor I could modify it. Ive tried res.clearCookie("cookie_name") or setting maxAge attribute to -1, nothing worked.
After googling and watching a lot of videos I found out that sending and receiving cookies from different domains is restricted because of security.
SOLUTION -> How did I fix the problem?
Well Ive came upon a VIDEO on YouTube, that showed how to host full-stack application on Heroku. That way your front end and back end are on the same domain and voilĂ sessions and cookies are working properly. Basically it goes like this:
1) Build your React app
2) In /server (main back end folder) create /public folder and move content of your /client/build directory here
3) In your back end main file (app.js in my case) add app.use(express.static("public")); to serve your front end.
4) Change cors settings to:
app.use(
cors({
origin: "your Heroku app url",
credentials: true, //allowing cookies
}));
5) Change express-session settings
app.use(
session({
name: "name of session",
key: "your key",
secret: "your secret",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
}));
6) Fix your routes for API calls (currently what Im working on)
7) Host it
Create session:
req.session.SESSION_NAME = session_data; Note that SESSION_NAME has to be identical with "name" attribute's value you have declared in step 5
Delete session:
res.clearCookie("SESSION_NAME"); req.session.destroy();
Hope this helps somebody, who encountered this issue.
I have a Vue.js SPA and a Node.js API built with Express.js. I'm using express-session (^1.11.3) to manage sessions and express-sequelize-session (0.4.0) to persist the session on a Postgres DB through Sequelize because I need a session to be able to use passport-azure-ad with oidc strategy.
I was having some issues logging in with Microsoft accounts after some time and came to the conclusion that it is because the session cookie (connect.sid) is never cleared from the browser.
I had some things misconfigured and made some changes but even with all the changes it is still not working.
Session on my Express app is configured in the following way:
import session from 'express-session';
import expressSequelizeSession from 'express-sequelize-session';
const Store = expressSequelizeSession(session.Store);
app.use(session({
cookie: {
path: '/',
httpOnly: true,
secure: env !== 'development', // On environments that have SSL enable this should be set to true.
maxAge: null,
sameSite: false, // Needs to be false otherwise Microsoft auth doesn't work.
},
secret: config.secrets.session,
saveUninitialized: false,
resave: false,
unset: 'destroy',
store: new Store(sqldb.sequelize),
}));
On the FE I'm using Vue.js with Axios and setting withCredentials to true so that the cookie is passed on the HTTP request.
// Base configuration.
import Axios from 'axios';
Axios.defaults.baseURL = config.apiURL;
Axios.defaults.headers.common.Accept = 'application/json';
Vue.$http = Axios;
// When making request.
Vue.$http[action](url, payload, { withCredentials: true }).then(() => // Handle request);
You can see from the image that the cookie is being sent on the logout request.
When logging out I'm hitting this endpoint and destroying the session as is explained on the documentation.
router.post('/logout', (req, res) => {
try {
req.session.destroy(() => {
return responses.responseWithResultAsync(res); // Helper method that logs and returns status code 200.
});
return responses.handleErrorAsync(res); // Helper method that logs and returns status code 500.
} catch (error) {
return responses.handleErrorAsync(res, error); // Helper method that logs and returns status code 500.
}
});
The interesting thing is that the session on the DB is removed so I know that the cookie is being sent properly on the request with the right session ID but it is not removing it on the browser for some reason. After logging out I still have this:
Does anyone know what I'm doing wrong? I find it odd that the session is being removed on the DB successfully but not on the request.
As #RolandStarke mentioned the express-session library doesn't have the built in functionality to remove the cookie from the browser, so I just did it manually in the following way:
router.post('/logout', (req, res) => {
try {
if (req.session && req.session.cookie) {
res.cookie('connect.sid', null, {
expires: new Date('Thu, 01 Jan 1970 00:00:00 UTC'),
httpOnly: true,
});
req.session.destroy((error) => {
if (error) {
return responses.handleErrorAsync(res, error);
}
return responses.responseWithResultAsync(res);
});
}
return responses.responseWithResultAsync(res);
} catch (error) {
return responses.handleErrorAsync(res, error);
}
});
I am using the npm package activedirectory to connect to my company's domain controller for the purpose of authenticating users on an internal website. The plan is for users to enter their windows domain credentials into the website's login page (written with React). The website will forward the credentials to a backend Express server that looks like this:
const express = require('express');
const bodyParser = require('body-parser');
const ActiveDirectory = require('activedirectory');
const app = express();
app.use(bodyParser.urlencoded({
extended: false,
}));
app.get('/ldap', (request, response) => {
// set http header for json
response.setHeader('ContentType', 'application/json');
// get the username and password from the query
const username = request.query.username + '#internal.mycompany.com';
const password = request.query.password;
console.log(username, password);
// create a new active directory connection
let activeDirectory = new ActiveDirectory({
url: 'LDAP://internal.mycompany.com',
baseDN: 'DC=mycompany,DC=com',
});
// attempt authentication
activeDirectory.authenticate(username, password, (error, authenticated) => {
if(error) {
console.log('ERROR: ' + JSON.stringify(error));
}
else {
console.log('Authentication successful');
}
// respond
response.send(JSON.stringify({
error: error,
authenticated: authenticated,
}));
});
});
app.listen(3001, () => {
console.log('Express server running on port 3001.');
});
The React frontend executes this when the 'Log In' button is clicked:
login() {
console.log('authenticating with username: ', this.state.username, ' and password: ', this.state.password);
fetch(`/ldap/?username=${encodeURIComponent(this.state.username)}&password=${encodeURIComponent(this.state.password)}`).then((response) => {
return response.json();
}).then((response) => {
console.log(response);
this.props.setLoggedIn(response.authenticated);
this.setState({
loginFailed: !response.authenticated,
redirectToHome: response.authenticated,
});
});
}
It calls fetch to ask the Express server to authenticate the credentials, and sets some global loggedIn state accordingly. It also sets local state to display an error message if the login attempt failed or redirect the user on to the main website homepage if the login attempt was successful.
Leaving the username and password blank yields this response:
{
error: {
code: 49,
description: "The supplied credential is invalid",
errno: "LDAP_INVALID_CREDENTIALS",
},
authenticated: false,
}
Typing in a valid username and password yields this response:
{
error: null,
authenticated: true,
}
This is all as expected up to this point.
However, typing in random characters for the username and password yields one of 2 responses. It either authenticates successfully, which shouldn't happen, or it gives this:
{
error: {
lde_dn: null,
lde_message: "80090308: LdapErr: DSID-0C0903A9, comment: AcceptSecurityContext error, data 52e, v1db1",
},
authenticated: false,
}
QUESTION:
Why would giving the domain controller garbage credentials cause it to return authenticated: true? And why only sometimes? Some information must be cached or remembered somewhere. I tried restarting the Express server and I tried waiting a day for something to expire.
Note: I was planning to encrypt/scramble the passwords so that they are not being sent in plain text, but if there is an even better way to send them securely, please leave a comment about how to improve this. But for now the main question is about the incorrect Active Directory/LDAP authentication.
This information: https://www.rfc-editor.org/rfc/rfc4513#section-6.3.1 essentially tells us that getting authenticated: true may not actually mean anything. The solution I came up with is to try and query the Active Directory to see if the user that just authenticated exists and/or try doing an/some operation(s) as the newly-authenticated user. If the operation(s) fail(s), then the login was not valid.
Additionally, there was an error in the above code. baseDN should have been 'DC=internal,DC=mycompany,DC=com'.
I've hit a bit of a problem getting client-session middleware working in Express. In short the session_state doesn't seem to be accessible when redirecting to new route after being set. For reference I have followed this video tutorial (client-session part starts 36:00 approx.) and double checked my steps but still encountering problems. Middleware is set up as follows:
var sessions = require('client-sessions');
Instantiated with code from the Express website.
app.use(sessions({
cookieName: 'session',
secret: 'iljkhhjfebxjmvjnnshxhgoidhsja',
duration: 24 * 60 * 60 * 1000,
activeDuration: 1000 * 60 * 5
}));
I have the sessions middleware placed between bodyParser and routes if that makes any difference.
Here are the sections my routes/index.js pertaining to the issue. The req.session_state seems to get set ok and the correct user details log to the console.
// POST login form
router.post('/login', function(req, res) {
User.findOne( { email: req.body.email }, function(err,user){
if(!user) {
console.log("User not found...");
res.render('login.jade',
{ message: 'Are you sure that is the correct email?'} );
} else {
if(req.body.password === user.password) {
// User gets saved and object logs correctly in the console
req.session_state = user;
console.log("session user...", req.session_state);
res.redirect('/dashboard');
}
}
//res.render('login.jade', { message: 'Invalid password'} );
});
});
However, something is going wrong when the res.redirect('/dashboard'); is run because the session_state is not accessible when it hits that route. Here is the code for the /dashboard route.
router.get('/dashboard', function(req, res) {
// OUTPUT = 'undefined' ???
console.log("dash...", req.session_state);
// 'if' fails and falls through to /login redirect
if(req.session && req.session_state){
console.log("dash route...", req.session_state);
User.findOne( { email: req.session_state.email }, function
(err, user){
if(!user){
req.session.reset();
res.redirect('/login');
} else {
res.locals.user = user;
res.render('dashboard.jade')
}
});
} else {
res.redirect('/login');
}
//res.render('dashboard', { title: 'Your Dashboard' });
});
Basically, the object stored in session_state is not accessible after the /dashboard redirect. I've been trying to debug it for a day or so without any luck. Any help much appreciated. Sorry if I am missing something obvious. Just getting my feet wet with session middleware so maybe I haven't fully grasped Session or I am overlooking something. Thanks in advance!
I've updated my answer with code that should help you set the cookie and an alternative session manager known as a token. In this example I've provided to parts to a middle ware one part which attaches the cookie (this can be expanded on to determine your use case) and the second part which checks the token for expiration or what ever else might be in there (i.e. audience, issuer, etc.)
app.use('/js/',function(req, res, next) {
//check if the request has a token or if the request has an associated username
if(!req.headers.cookie){
console.log('no cookies were found')
var token = jwt.sign({user_token_name:req.body.user_name},app.get('superSecret'), {
expiresIn: 1 *100 *60 // expires in 1 mintue can be what ever you feel is neccessary
});
//make a token and attach it to the body
// req.body.token = token // this is just placing the token as a payload
res.cookie('austin.nodeschool' , token,{ maxAge: 100000, httpOnly: true }) //this sets the cookie to the string austin.nodeschool
}
if(req.body.user_name){
next()
}else{
res.send('request did not have a username').end() // this is here because my middleware also requires a username to be associated with requests to my API, this could easily be an ID or token.
}
},function(req, res, next) {
// console.log(req.headers) this is here to show you the avilable headers to parse through and to have a visual of whats being passed to this function
if(req.headers.cookie){
console.log(req.headers.cookie) //the cookie has the name of the cookie equal to the cookie.
var equals = '=';
var inboundCookie = req.headers.cookie
var cookieInfo = splitCookie(inboundCookie,equals) //splitCookie is a function that takes the inbound cookie and splits it from the name of the cookie and returns an array as seen below.
console.log(cookieInfo)
var decoded = jwt.verify(cookieInfo[1], app.get('superSecret'));
console.log(decoded)
// You could check to see if there is an access_token in the database if there is one
// see if the decoded content still matches. If anything is missing issue a new token
// set the token in the database for later assuming you want to
// You could simply check if it's expired and if so send them to the login if not allow them to proceed through the route.
}
next()
});