On a call to an unprotected route I am able to print and respond with the values at req.cookies
{
"cookies": {
"connect.sid": "s:PqWvDsoKLMeCyRd8pGN<removed>",
"testCookie": "testValue"
},
"signedCookies": {}
}
However this route returns 401 Unauthorized
router.get('/authCheck', passportWithLocalStrategy.authenticate('local'), (req: Request, res: Response, next: NextFunction) => {
res.status(204).send();
});
Does anyone know what could be causing the session cookie not to authenticate?
Here is my main server.ts file, de/serialization, and the middleware stack:
// called to set a cookie initially
passport.serializeUser((user: any, callback) => {
callback(null, user.id as string);
});
// called every time a request is made
passport.deserializeUser(async (userId: string, callback) => {
const pgClient = new PgClient();
try {
pgClient.connect();
const userRecord = (await pgClient.query('SELECT * FROM app_user WHERE CAST(id as text) = CAST($1 as text)', [userId])).rows[0];
pgClient.end();
callback(null, userRecord);
} catch (error) {
callback(error);
}
});
server
.use(cors())
.use(express.json())
.use(expressSession({ secret: process.env.SESSION_SECRET! }))
.use(cookieParser())
.use(passport.initialize())
.use(passport.session())
.use(rootRouter)
<other routers removed>
I have setup my Passport LocalStrategy as shown:
async function useDatabaseToVerifyUserAndPassword(localUserName: string,
localPassword: string, doneCallback: any) {
const pgClient = new PgClient();
try {
await pgClient.connect();
const queryResult = await pgClient.query(selectUserQuery, [localUserName]);
pgClient.end();
const userData: UserMatch = queryResult.rows[0];
if (typeof userData === 'undefined' || typeof userData.password_hash === 'undefined') {
return doneCallback(null, false);
}
const hashesMatch: boolean = await bcrypt.compare(localPassword, userData.password_hash);
if (hashesMatch) {
return doneCallback(null, userData);
}
return doneCallback(null, false); // username not found or passHash mismatch. Prints 401 UnAuth
} catch (error) {
return doneCallback(error, false);
}
}
const strategyOptions = {
usernameField: 'localUserName',
passwordField: 'localPassword',
};
const localStrategy = new LocalStrategy(strategyOptions, useDatabaseToVerifyUserAndPassword);
passport.use(localStrategy);
export default passport;
The above export is brought into the router file (for the route at /authCheck) as passportWithLocalStrategy. If I just import passport from the library folder to that file, the route breaks, hanging indefinitely.
Update
I have tried unprotecting the route and accessing req.isAuthenticated(). It always returns false even when the session cookie is there.
I see this information printed when logging req.session
Session Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true }
You should have a separate login route that calls passport.authenticate after that returns successfully passport will add req.session.passport value with the serialized userId. You only need to do this process once when logging in a user.
The /authCheck route can then be refactored with middleware that just checks that the user is still logged in.
router.get('/authCheck', (req: Request, res: Response, next: NextFunction) => {
// passport isAuthenticated method
if(req.isAuthenticated()){
//req.isAuthenticated() will return true if user is logged in
return res.status(204).send();
} else{
res.redirect("/login");
}
});
Related
i have a strange problem. i detected where the problem is, but i dont know how to solve it. i use cookie-parser, express-mysql-session, express-session, connect-flash, passport and etc in my project. to store sessions in mySQL database, i use express-mysql-session module. but this module makes a problem for connect-flash. this is where i config first middlewares:
const setFirstMiddleWares = (server: Express) => {
if (devMode) server.use(morgan("dev"));
server.use(bodyParser.urlencoded({ extended: false }));
server.use(cookieParser());
server.use(
expressSession({
secret: "secret",
saveUninitialized: false,
resave: false,
store: MySQLSessionStore, //the problem is exactly this line
})
);
server.use(flash());
server.use(passport.initialize());
server.use(passport.session());
server.use(setRenderConfig);
};
the problem that express-mysql-session make for connect-flash is that when i set a message with connect-flash in middlewares and then i redirect user to for example login page, at the first time no message will be displayed in view. but when i refresh the login page, the message will be displayed! but when i remove the store property of express-session config object, everything will be fine and the flash message will be displayed in login view immediately after user redirected to the login page and the login page doesnt need to be refreshed to display the error or success message! it is really strange behavior for flash-connect. you can see the important parts of my codes:
DB.ts:
mport mysql from "mysql2/promise";
import MySQLStoreSessionsStore from "express-mysql-session";
import * as expressSession from "express-session";
const dbConfig = {
host: process.env.DBHOST,
user: process.env.DBUSER,
password: process.env.DBPASSWORD,
port: Number(process.env.DBPORT),
database: process.env.DATABASENAME,
};
const connection = mysql.createPool(dbConfig);
const MySQLSessionStoreClass = MySQLStoreSessionsStore(expressSession);
const MySQLSessionStore = new MySQLSessionStoreClass({}, connection);
export { connection as mysql, MySQLSessionStore };
Users.ts:
import { NextFunction, Request, Response } from "express";
import passport from "passport";
import { signUpUser } from "../models/Users";
const signUpUserController = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
await signUpUser(req.body);
req.flash("sign-in-successes", [
"your account has been created successfully! please login to your account.",
]);
res.redirect("/login");
} catch (errors) {
if (Array.isArray(errors)) {
req.flash("sign-up-errors", <string[]>errors);
res.redirect("/sign-up");
} else next({ error: errors, code: "500" });
}
};
const signInUserController = (
req: Request,
res: Response,
next: NextFunction
) => {
passport.authenticate("local", {
failureRedirect: "/login",
failureFlash: true,
})(req, res, next);
};
const remmemberUserController = (req: Request, res: Response) => {
console.log("line 5:", req.body);
if (req.body["remmember-user"]) {
req.session.cookie.originalMaxAge = 253402300000000;
} else {
req.session.cookie.expires = undefined;
}
res.redirect("/account/dashboard");
};
const logOutController = (req: Request, res: Response, next: NextFunction) => {
req.logOut({ keepSessionInfo: false }, (err) => {
console.log(err);
req.flash("sign-in-successes", ["logged out successfuly!"]);
res.redirect("/login");
});
};
export {
signUpUserController,
signInUserController,
logOutController,
remmemberUserController,
};
passport.ts:
import passport from "passport";
import localStrategy from "passport-local";
import bcrypt from "bcrypt";
import { getUsersInfos } from "../models/Users";
passport.use(
new localStrategy.Strategy(
{
usernameField: "user-email-signin",
passwordField: "user-password-signin",
},
async (email, password, done) => {
try {
const user: any = await getUsersInfos(email);
if (Array.isArray(user)) {
const length: 2 | number = user.length;
if (length === 0) {
return done(null, false, {
message: "user with your entered email not found!",
});
}
const isMatch = await bcrypt.compare(password, user[0].password);
if (isMatch) {
return done(null, user);
} else {
return done(null, false, {
message: "email or password are incorrect!",
});
}
} else {
return done(null, false, {
message: "something went wrong. please try again!",
});
}
} catch (error) {
console.log(error);
}
}
)
);
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser(async (user, done) => {
if (Array.isArray(user)) {
done(null, user[0].id);
}
});
LoginController.ts:
import { Request } from "express";
import { NewExpressResponse } from "../types/Types";
const loginController = (req: Request, res: NewExpressResponse) => {
res.render("login", {
title: `login`,
scripts: [...res.locals.scripts, "/js/LoginScripts.js"],
successes: req.flash("sign-in-successes"),
error: req.flash("error"),
});
};
export { loginController };
i searched a lot for this problem in google, but the only thing that i found is that i should use req.session.save(() => { res.redirect("/login");}) in my middlewares to display the flash message in the view immediately after user redirected to the route.
but there are some problems with this way:
i should use this code for every redirection if i want to set and use the flash message at the redirect route and i actually dont like this.
i cant do something like this in signInUserController because passport set the errors in flash by itself.
so do you have any idea to fix this strange behavior of connect-flash when im using express-mysql-session? thanks for help :)
I found that this is a problem(actually not a problem, a feature that can be added) in connect-flash. It doesnt save the session by itself. Because of it, i created the async version of this package with more features. You can use promise base or callback base functions to save and get your flash messages. So you can use async-connect-flash instead of using connect-flash. I hope you like it :)
I have a small app that allows registration and login, but I'm still trying to use session-express to persist the session.
Below is server.ts where I create the session, cors, etc...
import express, { json } from "express";
import { db } from "./models/db-connection";
import { router } from "./routes";
import session from "express-session";
var cors = require("cors");
const app = express();
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "GET"],
credentials: true,
})
);
app.use(json());
app.use(
session({
secret: "testtest",
resave: false,
saveUninitialized: false,
})
);
app.use(router);
app.listen(3001, async () => {
try {
await db.sync();
console.log("Connected to the database");
} catch (error) {
console.error("Failed to connect to the database", error);
}
});
In the routes.ts script I use the authenticate function which will only allow a new user to be registered if an user is already logged in.
But the problem is exactly here, req.session.authenticated is never true, it is always undefined, even when I set it to true as I will show in UserController.ts.
Below is routes.ts.
import express from "express";
import UserController from "./controllers/UserController";
import "./session-data";
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.redirect("/login");
}
}
const router = express.Router();
router.post("/users", authenticate, UserController.create);
router.get("/users/login/:login", UserController.findLogin);
export { router };
As you can see below in UserController.ts, req.session.authenticated is true when we find a match, I put in a console.log just to confirm that req.session has the authenticated property at this point, and it does, but it looks like routes.ts can't see it.
UserController.ts
import express, { Request, Response } from "express";
import { UserModel } from "../models/UserModel";
import "../session-data";
const bcrypt = require("bcryptjs");
class UserController {
async findLogin(req: express.Request, res: express.Response) {
const email = req.query.email?.toString();
const password = req.query.password?.toString();
try {
const user: any = await UserModel.findOne({
where: {
email: email,
},
});
if (user) {
const match = await bcrypt.compare(password, user.password);
if (match) {
req.session.authenticated = true;
console.log(req.session);
return res.status(204).json(user);
} else {
req.session.authenticated = false;
return res.status(200).send("invalid password");
}
} else {
req.session.authenticated = false;
return res.status(201).send("User not found");
}
} catch (error: any) {
req.session.authenticated = false;
return res.send(error.message);
}
}
}
async create(req: Request, res: Response) {
try {
const { userName, email, password } = req.body;
const user = await UserModel.create({
userName,
email,
password,
});
return res.status(201).json(user);
} catch (error: any) {
console.error(error);
return res.send(error.message);
}
}
Since I'm using Typescript, I need to create a session-data.ts file to expand req.session
session-data.ts
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
export {};
In the session store, the session is never created either.
Could you help me please? I don't know why req.session.authenticated isn't working, I'm new to using typescript, I imagine there's something related to that.
Thanks!
Try simplifying your code as much as possible so that the problem is still reproducible. The following works for me:
declare module "express-session" {
interface SessionData {
authenticated: boolean;
}
}
function login(req: express.Request, res: express.Response, next: express.NextFunction) {
req.session.authenticated = true;
res.end("Logged in");
}
function authenticate(req: express.Request, res: express.Response, next: express.NextFunction) {
console.log(req.session);
if (req.session.authenticated) {
next();
} else {
res.end("Not logged in");
}
}
express()
.use(session({
secret: "Se$$ion",
resave: false,
saveUninitialized: false
}))
.get("/login", login)
.get("/auth", authenticate, function(req, res) {
res.end("Welcome");
})
.listen(3001);
GET /login returns "Logged in".
Then GET /auth return "Welcome" and the session is logged:
Session {
cookie: { path: '/', _expires: null, originalMaxAge: null, httpOnly: true },
authenticated: true
}
If API is accessed as GET request from the browser as authenticated, you get the data.
But when you want to access the data by fetching in client-side by the function getserverside gives me the error that is not authenticated which it fact it is.
API
export default async (req, res) => {
const { method } = req;
const cookie = {
headers: {
cookie: req.headers["cookie"],
},
};
const session = await getSession({ req: cookie });
if (!session) {
return res.status(401).json({
success: false,
message: "NOT Authorized",
});
}
switch (method) {
case "GET":
try {
const jobs = await prisma.jobs.findMany({
where: {
userId: session.id,
},
});
return res.status(200).json({ success: true, jobs });
} catch (error) {
return res.status(404).json({
success: false,
message: error.message,
});
}
Component
export async function getServerSideProps({ req, res }) {
console.log(req.cookies); // Logs all cookies from the request
const cookie = {
headers: {
cookie: req.headers["cookie"],
},
};
const session = await getSession({ req: cookie });
console.log(session); // session is received perfectly.
const res = await axios.get(`${process.env.API_URL}/company/jobs`); // ERROR NOT AUTHENTICATED
}
It is a bit weird because when I try to access the data the cookie is received but when I make the request it says is not authenticated
According to documentation, you should pass to getSession function whole original req in API. and whole context prop in getServerSideProps
https://next-auth.js.org/getting-started/client#getsession
I moved from a express server handling my API's in Next to their built in API Routes!
Loving it!
Anyway, I am using Passport.js for authentication and authorization and have implemented that successfully.
But I noticed a few things which I want to bring first as I am pretty sure they're related to the problem with the SWR hook:
In my login route: /api/login:
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
import passport from '../../lib/passport'
import connectDB from '../../lib/mongodb';
const handler = nextConnect()
handler
.use(auth)
.post(
async (req, res, next) => {
await connectDB();
passport.authenticate('local', (err, user, info) => {
if (err) { return errorHandler(err, res) }
if (user === false) {
return res.status(404).send({
msg: `We were unable to find this user. Please confirm with the "Forgot password" link or the "Register" link below!`
})
}
if (user) {
if (user.isVerified) {
req.user = user;
return res.status(200).send({
user: req.user,
msg: `Your have successfully logged in; Welcome to Hillfinder!`
});
}
return res.status(403).send({
msg: 'Your username has not been verified! Check your email for a confirmation link.'
});
}
})(req, res, next);
})
export default handler
You can see I'm using the custom callback in Passport.js so when I get the user from a successful login I am just assigning the user to req.user = user
I thought this should allow the SWR hook to always return true that the user is logged in?
This is my hooks.js file i.e. SWR functionality:
import useSWR from 'swr'
import axios from 'axios';
export const fetcher = async (url) => {
try {
const res = await axios.get(url);
console.log("res ", res);
return res.data;
} catch (err) {
console.log("err ", err);
throw err.response.data;
}
};
export function useUser() {
const { data, mutate } = useSWR('/api/user', fetcher)
// if data is not defined, the query has not completed
console.log("data ", data);
const loading = !data
const user = data?.user
return [user, { mutate, loading }]
}
The fetcher is calling the /api/user:
import nextConnect from 'next-connect'
import auth from '../../middleware/auth'
const handler = nextConnect()
handler
.use(auth)
.get((req, res) => {
console.log("req.user ", req.user); // Shouldn't the req.user exist??
res.json({ user: req.user })
})
export default handler
Shouldn't that always return the user from a successful login?
Lastly here is my LoginSubmit:
import axios from 'axios';
export default function loginSubmit(
email,
password,
router,
dispatch,
mutate
) {
const data = {
email,
password,
};
axios
.post(`/api/login`,
data, // request body as string
{ // options
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
}
)
.then(response => {
const { userId, user } = response.data
if (response.status === 200) {
setTimeout(() => {
router.push('/profile');
}, 3000);
dispatch({ type: 'userAccountIsVerified' })
mutate(user)
}
})
}
Any help would be appreciated!
Update
Added auth middleware to question:
import nextConnect from 'next-connect'
import passport from '../lib/passport'
import session from '../lib/session'
const auth = nextConnect()
.use(
session({
name: 'sess',
secret: process.env.TOKEN_SECRET,
cookie: {
maxAge: 60 * 60 * 8, // 8 hours,
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
},
})
)
.use((req, res, next) => {
req.session.users = req.session.users || []
next()
})
.use(passport.initialize())
.use(passport.session())
export default auth
Added: serializeUser && deserializeUser functions;
import passport from 'passport'
import LocalStrategy from 'passport-local'
import User from '../models/User'
passport.serializeUser((user, done) => {
done(null, user._id);
});
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
passport.use(
new LocalStrategy(
{ usernameField: 'email', passwordField: 'password', passReqToCallback: true },
async (req, email, password, done) => {
try {
const user = await User.findOne({ email }).exec();
if (!user) {
return done(null, false, { message: 'Invalid username!' });
}
const passwordOk = await user.comparePassword(password);
if (!passwordOk) {
return done(null, false, {
message: 'Invalid password!'
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}
)
);
export default passport
And this is the session.js file:
import { parse, serialize } from 'cookie'
import { createLoginSession, getLoginSession } from './auth'
function parseCookies(req) {
// For API Routes we don't need to parse the cookies.
if (req.cookies) return req.cookies
// For pages we do need to parse the cookies.
const cookie = req.headers?.cookie
return parse(cookie || '')
}
export default function session({ name, secret, cookie: cookieOpts }) {
return async (req, res, next) => {
const cookies = parseCookies(req)
const token = cookies[name]
let unsealed = {}
if (token) {
try {
// the cookie needs to be unsealed using the password `secret`
unsealed = await getLoginSession(token, secret)
} catch (e) {
// The cookie is invalid
}
}
req.session = unsealed
// We are proxying res.end to commit the session cookie
const oldEnd = res.end
res.end = async function resEndProxy(...args) {
if (res.finished || res.writableEnded || res.headersSent) return
if (cookieOpts.maxAge) {
req.session.maxAge = cookieOpts.maxAge
}
const token = await createLoginSession(req.session, secret)
res.setHeader('Set-Cookie', serialize(name, token, cookieOpts))
oldEnd.apply(this, args)
}
next()
}
}
I have my Express + Passport + Firebase project where I handle authentication with a local stratetegy. Since I found that Passport would take care of the authentication process, so I also found that it would accept flash messages as third parameter for the done() function (in the strategy). But I am not sure how to read them:
I guess the flow I made to set and read flash messages were:
Install connect-flash with NPM.
Set the Express middleware after importing it:
import * as flash from 'connect-flash';
...
const app = express();
...
app.use(flash());
Configure Passport Authentication in the Express route according to the documentation:
// POST - /api/v1/admin/oauth/login
router.post(
'/login',
async (req: Request, res: Response) => { /* middleware function to validate input */ },
passport.authenticate('local', {
failureRedirect: '/api/v1/admin/oauth/login',
failureFlash: true
}),
async (req: Request, res: Response) => { /* function after success login */
);
Include the flash messages in the done() method, according to Passport configuration documentation:
import { Strategy as LocalStrategy } from 'passport-local';
import db from '../../config/database';
import * as bcrypt from 'bcryptjs';
export default new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
const ref = db.collection('users').doc(email);
try {
const doc = await ref.get();
if (!doc.exists) {
return done(null, false, { error: 'Wrong email' });
}
const user = doc.data();
const match: boolean = await bcrypt.compare(password, user.password);
if (!match) {
return done(null, false, { error: 'Wrong password' });
}
user.id = doc.id;
delete user.password;
return done(null, user);
} catch(error) {
return done(error);
}
});
Read the flash messages using req.flash('error'):
// GET - /api/v1/admin/oauth/login
router.get('/login', (req: any, res: Response) => {
const result: IResult = {
message: '',
data: null,
ok: false
};
if (req.flash('error')) {
resultado.message = req.flash('error');
console.log(req.flash('error'));
}
return res.status(400).json(result);
});
I thought it was theroically working in my mind, until step 5, where req.flash('error') has an empty array in it. What I am doing wrong?
You're passing the flash message wrong!
The 3rd argument of done() should be an object with the fields type and message:
return done(null, false, { message: 'Wrong email' });
The type defaults to error.
This API doesn't seem to be documented explicitly, but is shown in the 3rd example of the Verify Callback section in the Configure chapter of the Passport.js documentation.
I've created a repo with a minimally reproducible working example.
I keep searching and I found a solution but it works in the second login attempt.
Steps from my question I modified to make it work:
Install connect-flash with NPM.
Set the Express middleware after importing it:
import * as flash from 'connect-flash';
...
const app = express();
...
app.use(flash());
Configure Passport Authentication in the Express route according to the documentation:
// POST - /api/v1/admin/oauth/login
router.post(
'/login',
async (req: Request, res: Response) => { /* middleware function to validate input */ },
passport.authenticate('local', {
failureFlash: true,
failureRedirect: '/api/v1/admin/oauth/login'
}),
async (req: Request, res: Response) => { /* function after success login */
);
Create another route so it can display the flash message, thanks to #Codebling:
// GET - /api/v1/admin/oauth/login
router.get('/login', (req: Request, res: Response) => {
const result: IResult = {
message: 'Auth ok',
data: null,
ok: true
};
let status: number = 200;
const flashMessage: any = req.flash('error');
if (flashMessage.length) {
resultado.message = flashMessage[0];
resultado.ok = false;
status = 400;
}
return res.status(status).json(result);
});
Include the flash messages in the done() method, according to Passport configuration documentation:
import { Strategy as LocalStrategy } from 'passport-local';
import db from '../../config/database';
import * as bcrypt from 'bcryptjs';
export default new LocalStrategy({ usernameField: 'email' }, async (email, password, done) => {
const ref = db.collection('users').doc(email);
try {
const doc = await ref.get();
if (!doc.exists) {
return done(null, false, { message: 'Wrong email' });
}
const user = doc.data();
const match: boolean = await bcrypt.compare(password, user.password);
if (!match) {
return done(null, false, { message: 'Wrong password' });
}
user.id = doc.id;
delete user.password;
return done(null, user);
} catch(error) {
return done(error);
}
});