I have a simple MEAN app and I want to implement a simple "home-made" user authentication. My idea is to save the userId in the session when he logs in, and to check if userId exists in the session on each page request (for example, when getting the list of all users).
Backend - server.js:
const express = require("express");
const session = require("express-session");
const bodyParser = require("body-parser");
const cors = require("cors");
const app = express();
var MemoryStore = session.MemoryStore;
app.use(
session({
name: "app.sid",
secret: "my_s3cr3t",
resave: true,
store: new MemoryStore(),
saveUninitialized: true
})
);
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(cors());
const dbConfig = require("./config/database.config.js");
const mongoose = require("mongoose");
mongoose.Promise = global.Promise;
mongoose
.connect(dbConfig.url)
.then(() => {
// ...
})
.catch(err => {
// ...
process.exit();
});
require("./app/routes/user.routes.js")(app);
require("./app/routes/task.routes.js")(app);
require("./app/routes/login.routes.js")(app);
app.listen(3333, () => {
console.log("Server is listening on port 3333");
});
When a user clicks the Login button, a method from the frontend controller is called:
Frontend - login.controller.js:
vm.login = function() {
userService.getUserByUsername(vm.username).then(user => {
if (user.password === vm.password) {
console.log("Login ok");
loginService.login(user).then(($window.location.href = "/#!main"));
} else {
console.log("Login not ok");
}
});
};
Backend - login.controller.js:
exports.login = (req, res) => {
req.session.userId = req.body._id;
req.session.save(function(err) {
console.log(err); // prints out "undefined", so there's no error
});
console.log(req.session);
res.status(200).send({
message: "Login ok"
});
};
The frontend LoginController prints out "Login ok" (assuming that I entered correct credentials) and redirects me to the "main" page which uses main.controller.js:
In the meantime, the backend login controller prints out the following:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true },
userId: '5b4746cafe30b423181ad359' }
So there is definitely a userId in the session content. However, when I get redirected to the main.html and the main.controller.js gets invoked, it calls:
loginService.getSession().then(data => console.log(data));
(I just want to check if the userId is still in the session, and later I will perform some useful actions)
The getSession() method in the frontend LoginService only does the $http call:
function getSession() {
return $http.get("http://localhost:3333/session").then(
function(response) {
return response.data;
},
function(error) {
console.log(error.status);
}
);
}
This one calls the method which is defined in the backend LoginController:
exports.getSession = (req, res) => {
console.log(req.session);
if (req.session.userId) {
res
.status(200)
.send({ message: "Session existing with userId " + req.session.userId });
} else {
res.status(404).send({ message: "Session not existing" });
}
};
The frontend call prints the status code 404 in the console, while in the backend I get the following output:
Session {
cookie:
{ path: '/',
_expires: null,
originalMaxAge: null,
httpOnly: true } }
(no userId is present...)
One more thing... In a few tutorials I saw that they are using cookie-parser. However, when I try to use it, I don't get any data from my database, only the static text is displayed. So I removed it temporarily from server.js.
EDIT:
I tried adding MongoStore to my app:
const MongoStore = require("connect-mongo")(session);
...
app.use(
session({
name: "app.sid",
secret: "G4m1F1c4T10n_#ppL1c4t10N",
resave: true,
saveUninitialized: false,
cookie: { maxAge: 600000 },
store: new MongoStore({ url: "mongodb://localhost:27017/myAppDb" })
})
);
...but nothing changed.
How can I get my sessions to work?
As I found out after talking to several people, sessions are more or less deprecated and the new way of handling these things are the tokens. So I switched to JWT and it's working great.
Related
First of all, please understand that I am not good at English.
I am currently implementing authentication using NextJS (full framework) + passport.
I succeeded in getting the user's Twitter information using strategy, and after that, I'm going to jwt the user's information in db and deliver it to you as a cookie, but I don't know when and where this should be done.
code is like
lib/passport.ts
passport.use(
new TwitterStrategy(
{
consumerKey: process.env.TWITTER_CONSUMER_KEY as string,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string,
callbackURL: '/api/auth/callback/twitter',
includeEmail: true,
},
async (_accessToken, _refreshToken, profile: TwitterProfile, cb: any) => {
try {
return cb(null, profile);
} catch (e: any) {
throw new Error(e);
}
}
)
);
req.session.passport
passport.serializeUser((user, cb) => {
process.nextTick(function () {
return cb(null, user);
});
});
passport.deserializeUser(function (
user: any,
cb: (arg0: null, arg1: any) => any
) {
process.nextTick(function () {
return cb(null, user);
});
});
middleware/auth.ts
const auth = nextConnect()
.use(
session({
secret: process.env.NEXTAUTH_SECRET,
cookie: {
maxAge: 60 * 60 * 8, // 8 hours,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
},
resave: false,
saveUninitialized: true,
})
)
.use((req, res, next) => {
// Initialize mocked database
// Remove this after you add your own database
req.session.users = req.session.users || [];
next();
});
api/auth/callback/twitter.ts
import nextConnect from 'next-connect';
import passport from '#/lib/passport-twitter';
import auth from '#/middleware/auth';
const handler = nextConnect();
handler
.use(auth)
.use(passport.session())
.get((req, res) => {
passport.authenticate('twitter', async () => {
// I tried to set header here
res.setHeader('Set-Cookie', 'test');
res.end();
});
});
And when I try to log in additionally, where can I check the client request's cookies? (for access_token verification).
Lack of knowledge may lead to a lack of explanation and a wrong approach.
I want to use authentication information to store user information according to the situation. For example, if you have another account stored in the same email (Facebook), you can send a specific message without creating a user.
I am following a tutorial of how to creat a blogapp using expressjs, mongoDB and handlebars.
The instructor uses req.flash() in a req.redirect command and it works for him, but when I try it, my page does not display the message.
The problem is that when I use req.flash(type, message) with req.render it works perfectly, but my page does not reload as I need.
If I check the flash message calling it on a console log it shows me message.
// Session
app.use(session({
secret: 'secret',
resave: true,
saveUninitialized: true,
cookie: {secure: true}
}));
app.use(flash());
// Middleware
app.use((req, res, next) =\> {
res.locals.success_msg = req.flash(('success_msg')\[0\]);
res.locals.error_msg = req.flash(("error_msg")\[0\]);
next();
});
adminRouter.post('/categories/delete', async (req, res) =\> {
try {
const { id } = req.body;
await Category.deleteOne({\_id: id });
req.flash('success_msg', 'Category deleted successfully');
res.redirect("/admin/categories");
} catch (err) {
req.flash('error_msg', `Error to delete category: ${err}`);
res.redirect('admin/categories');
}
})
my render:
{{#if success_msg}}
\<div class="alert alert-success"\>{{success_msg}} \</div\>
{{else if error_msg}}
\<div class="alert alert-danger"\> {{error_msg}} \</div\>
{{/if}}
I found the error after reading this article
My problem was the session setup.
This was my solution:
// Session
var sessionStore = new session.MemoryStore;
app.use(cookieParser('secret'));
app.use(session({
cookie: { maxAge: 60000 },
store: sessionStore,
saveUninitialized: true,
resave: 'true',
secret: 'secret'
}));
app.use(flash());
// Middleware
app.use((req, res, next) => {
//res.locals.success_msg = req.flash(('success_msg')[0]);
res.locals.success_msg = req.flash('success_msg');
//res.locals.error_msg = req.flash(("error_msg")[0]);
res.locals.error_msg = req.flash("error_msg");
next();
});
So I feel like I've tried everything.
Quick workflow breakdown. I have a React App deployed on Netlify that uses a Rest API backend hosted on Heroku with a Jaws MySQL instance and a Redis session store. There is an authorization workflow that uses a google0auth2.0 strategy and passport.js as well as the Redis to store each separate session which I only started using on the production build as my understanding was that express-sessions alone would cause memory leaks. That was my first step through this rabbit hole.
After some research, I added app.set('trust proxy', 1), added app.use(helmet.hsts()) to address headers issues and I attempted to secure the cookie with the code below:
app.use(
expressSession({
...
proxy: isProdEnv,
cookie: {
secure: isProdEnv
}
When checking the network tab in chrome dev tools, I see the cookie is attached to the callback however, it is not attached to the dashboard react page that the API redirects on the successful authorization.
My API index.js is below:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const expressSession = require('express-session');
const {
createClient
} = require("redis");
let RedisStore = require('connect-redis')(expressSession);
require('dotenv').config();
const PORT = process.env.PORT || 5050;
const isProdEnv = process.env.NODE_ENV === 'production' ? true : false;
// Knex instance for DB managment
const knex = require('knex')(require('./knexfile.js')[process.env.NODE_ENV || 'development']);
const app = express();
app.use(express.json());
app.use(helmet());
app.use(helmet.hsts());
app.use(
cors({
origin: true,
credentials: true,
}),
);
let redisClient = createClient({
url: process.env.REDIS_URL,
lazyConnect: true,
showFriendlyErrorStack: true,
legacyMode: true,
retry_strategy: (options) => {
const {
error,
total_retry_time,
attempt
} = options;
if (error ? .code === 'ECONNREFUSED' || error ? .code === 'NR_CLOSED') {
return 5000;
}
if (total_retry_time > 1000 * 15) {
return undefined;
}
if (attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 1000, 5000); //in ms
},
});
if (!redisClient.isOpen) {
redisClient.connect().catch(console.error);
console.info('connected to redis at', process.env.REDIS_URL);
}
redisClient.on('error', (err) => {
console.log('ⓘ on error:', err);
});
app.set('trust proxy', 1);
// Include express-session middleware (with additional config options required
// for Passport session)
app.use(
expressSession({
store: new RedisStore({
client: redisClient
}),
secret: process.env.SESSION_SECRET,
proxy: isProdEnv,
resave: false,
saveUninitialized: true,
name: 'lostnfound',
cookie: {
secure: isProdEnv
}
}),
);
// =========== Passport Config ============
// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['profile', 'email'],
},
(_accessToken, _refreshToken, profile, done) => {
const id = String(profile.id);
const profileId = Number(id.slice(-18));
// First let's check if we already have this user in our DB
knex('users')
.select('id')
.where({
google_id: profileId
})
.then((user) => {
if (user.length) {
// If user is found, pass the user object to serialize function
done(null, user[0]);
} else {
// If user isn't found, we create a record
knex('users')
.insert({
google_id: profileId,
avatar_url: profile._json.picture,
first_name: profile.name.givenName,
last_name: profile.name.familyName,
email: profile._json.email,
})
.then((userId) => {
// Pass the user object to serialize function
done(null, {
id: userId[0]
});
})
.catch((err) => {
console.log('Error creating a user', err);
});
}
})
.catch((err) => {
console.log('Error fetching a user', err);
});
},
),
);
// `serializeUser` determines which data of the auth user object should be stored in the session
// The data comes from `done` function of the strategy
// The result of the method is attached to the session as `req.session.passport.user = 12345`
passport.serializeUser((user, done) => {
console.log('serializeUser (user object):', user);
// Store only the user id in session
done(null, user.id);
});
// `deserializeUser` receives a value sent from `serializeUser` `done` function
// We can then retrieve full user information from our database using the userId
passport.deserializeUser((userId, done) => {
console.log('deserializeUser (user id):', userId);
// Query user information from the database for currently authenticated user
knex('users')
.where({
id: userId
})
.then((user) => {
// Remember that knex will return an array of records, so we need to get a single record from it
console.log('req.user:', user[0]);
// The full user object will be attached to request object as `req.user`
done(null, user[0]);
})
.catch((err) => {
console.log('Error finding user', err);
});
});
// Additional information on serializeUser and deserializeUser:
// https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize
// Import all route types for server functionality
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/post');
app.use('/auth', authRoutes);
app.use('/post', postRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server listening on port ${PORT}.`);
});
And this is my google endpoint and google callback:
const express = require('express');
const router = express.Router();
const passport = require('passport');
require('dotenv').config();
const AuthController = require('../controller/auth');
// Create a login endpoint which kickstarts the auth process and takes user to a consent page
router.get('/google', passport.authenticate('google'));
// This is the endpoint that Google will redirect to after user responds on consent page
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
}),
(_req, res) => {
// Successful authentication, redirect to client-side application
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
},
);
I will add, I'm on the free Heroku account and haven't set up any SSL. Is that what is holding me back? is there any third-party free SSL that I can work into the workflow?
It's hosted here if you want to get a first-hand look. Click on the login to google to go through the workflow and checkout the network tab after you're authorized (if you dare, lol)
I am using facebook and google oauth2 login using passport js, with this flow
User clicked the login button
Redirects to facebook/google auth page (depending on what login the user chooses)
The auth page redirects back to a callback page (/auth/callback/[provider])
A passport express middleware will catch it to parse some data and then send it to a remote api of myown to sign the user in
The auth remote api will send a response back consisting the user token
A custom express middleware will catch the response to set cookie on the server
the express chain ends by route it to /profile (cookie with token is set on the browser)
/profile will then checks if there is a token, if there is not: it will redirect to /
Doing this flow on facebook login is fine, the user is successfully redirected to /profile, with all of its data and token, the google oauth2 login however seems to be doing the redirect to /profile then setting the token (step #7 then #6), so everytime the user is using google oauth2 login, its always gonna be redirected back to / since by the time it arrives at /profile, it doesnt have the token
here's the code on the above's flow
#./server.js
const express = require('express')
const next = require('next')
const Passport = require('./server/middleware/passport')
const Api = require('./server/api')
const port = parseInt(process.env.PORT, 10)
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
app
.prepare()
.then(() => {
const server = express()
// ... other unrelated things
server.use(Passport.initialize())
Api.passport.facebook(server)
Api.passport.facebookCallback(server)
Api.passport.google(server)
Api.passport.googleCallback(server)
// ... other unrelated things
server.all('*', (req, res) => handle(req, res))
server.listen(port, (error) => {
if (error) throw error
// ... other unrelated things
})
})
#./server/api.js
const Passport = require('middleware/passport')
function setCookie(req, res, next) {
res.cookie('token', req.user.auth.token, {
httpOnly: true,
sameSite: 'strict',
path: '/',
secure: process.env.NODE_ENV !== 'development',
})
next()
}
function facebook(app) {
return app.get('/auth/facebook', (req, res, next) => {
Passport.authenticate('facebook', {
scope: ['email', 'public_profile']
})(req, res, next)
})
}
function facebookCallback(app) {
return app.get(
'/auth/callback/facebook',
Passport.authenticate('facebook', { session: false, failureRedirect: '/' }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
function google(app) {
return app.get('/auth/google', (req, res, next) => {
Passport.authenticate('google', {
scope: [
'https://www.googleapis.com/auth/userinfo.email ',
'https://www.googleapis.com/auth/userinfo.profile ',
],
prompt: 'consent',
authType: 'rerequest',
accessType: 'offline',
})(req, res, next)
})
}
function googleCallback(app) {
return app.get(
'/auth/callback/google',
Passport.authenticate('google', { failureRedirect: '/', session: false }),
setCookie,
(req, res) => {
res.redirect('/profile')
},
)
}
module.exports = {
passport: {
facebook,
facebookCallback,
google,
googleCallback,
}
}
#./server/middleware/passport.js
const axios = require('axios')
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const FacebookStrategy = require('passport-facebook').Strategy
passport.serializeUser((user, done) => {
done(null, user)
})
passport.deserializeUser((obj, done) => {
done(null, obj)
})
function verifyCallback(req, ... , done) {
process.nextTick(async () => {
try {
const options = {
baseURL: baseUrl, // My remote api url
method: 'POST',
url: '/auth/signin',
headers: {
'Content-Type': 'application/json',
},
data: JSON.stringify({
// email, fullname, etc
}),
}
const response = await axios(options)
return done(null, response.data)
} catch (error) {
const { response } = error
return done(JSON.stringify(response.data, null, 2), null)
}
})
}
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: callbackURLGoogle,
passReqToCallback: true,
}, verifyCallback))
passport.use(new FacebookStrategy({
clientID: process.env.FACEBOOK_CLIENT_ID,
clientSecret: process.env.FACEBOOK_CLIENT_SECRET,
callbackURL: callbackURLFacebook,
enableProof: true,
profileFields: ['id', 'name', 'email', 'picture.type(large)'],
passReqToCallback: true,
}, verifyCallback))
module.exports = passport
I console.log() things, just to figure out if it falls to the correct sequence of flow, the console doesn't seem to log anything suspicious, is there's something i am missing here?
PS: i am also using next js with custom server
I was facing the same problem and was able to send cookies by using custom callback.
router.get('/google/callback', (req, res) => {
passport.authenticate('google', {session: false, failureRedirect:'/auth/google/failure'},
async(err, user) => {
// You can send cookies and data in response here.
})(req, res)
})
Please refer custom callback section in documentation for explanation.
I'm new to NUXT and SSR and I've been researching this for a few hours now and I can't seem to figure it out. I'm using JWT to authenticate users in my Nuxt app with a Bearer Token, which is working great until I hit refresh and lose my session.
Now I'm looking to persist sessions using express-session and connect-mongo. I can't get the cookie to set on the client to be included on future requests.
When a user is authenticated:
router.post('/login', function(req, res) {
User.findOne({
username: req.body.username
}, function(err, user) {
if (err) throw err;
if (!user) {
res.status(401).send({success: false, msg: 'Authentication failed. User not found.'});
} else {
// check if password matches
user.comparePassword(req.body.password, function (err, isMatch) {
if (isMatch && !err) {
// if user is found and password is right create a token
var token = jwt.sign(user.toJSON(), config.secret, { expiresIn: 604800 });
req.session.authUser = { 'user': 'Test User' }
return res.json({success: true, token: token, user: user});
} else {
res.status(401).send({success: false, msg: 'Authentication failed. Wrong password.'});
}
});
}
The console.log above shows the authUser in the session.
Session {
cookie:
{ path: '/',
_expires: 2018-04-03T18:13:53.209Z,
originalMaxAge: 60000,
httpOnly: true },
authUser: { user: 'Test User' } }
When I look at my chrome devtools application cookies a connect.ssid hasn't been set and when I console.log(req.session) on future requests the authUser is missing.
My server code is:
// Passport
var passport = require('passport');
var passportJWT = require("passport-jwt");
var ExtractJwt = passportJWT.ExtractJwt;
var JwtStrategy = passportJWT.Strategy;
// Config File
let config = require('./config/settings.js')
// Initialize Express
var app = express();
// CORS-ENABLE
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:1337");
res.header("Access-Control-Allow-Credentials", 'true');
next();
});
app.use(cors())
const dbPath = 'mongodb://blogUser:blogUserPassword#localhost:27017/blog'
// Express Session
app.use(session({
secret: 'super-secret-key',
resave: false,
saveUninitialized: false,
store: new MongoStore({ url: dbPath }),
cookie: { maxAge: 60000 }
}))
// File Upload
app.use(fileUpload());
// view engine setup
// app.set('views', path.join(__dirname, 'views'));
// app.set('view engine', 'jade');
// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Routes
var index = require('./routes/index');
var users = require('./routes/users');
app.use('/api', index);
app.use('/users', users);
// Passport Config
app.use(passport.initialize());
app.use(passport.session())
// mongoose
const options = {
autoIndex: true, // Don't build indexes
reconnectTries: Number.MAX_VALUE, // Never stop trying to reconnect
reconnectInterval: 500, // Reconnect every 500ms
poolSize: 10, // Maintain up to 10 socket connections
// If not connected, return errors immediately rather than waiting for reconnect
bufferMaxEntries: 0
};
console.log(options);
// Localhost Connect
mongoose.connect(dbPath, options).then(
() => { console.log("connected !!!"); },
err => { console.log(err); }
);
Any and all help is appreciated.
If you want to use the server you create the problem with the session is the express router, because change res and req vars so like recommend in nuxt use this.
const express = require('express');
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
const app = express()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
You are missing this step
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
// So we can use res.status() & res.json()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})
The req, res parameters need to be interchanged on the client side
Once you do router.post('/login') and logout
app.use('/api', router)
And that will work perfectly