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);
}
});
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 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()
}
}
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");
}
});
I am using this passport strategy.
passport.use(
'onlyForRefreshToken',
new JWTStrategy(
{
jwtFromRequest: ExtractJWT.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecretRider,
},
(jwtPayload, done) => {
if (jwtPayload) {
return done(null, jwtPayload);
}
return done(null, false);
},
),
);
My goal is Putting 'jwtPayload' into my rest API of Nodejs that is located at other folder.
That is, I want to use jwtPayload decoded at the code below.
exports.riderRefreshToken = async (req, res) => {
const { email } = req.body;
const exRiderRefreshToken = await Rider.findOne({ email });
}
And this router works by middleware of jwtstrategy.
router.post(
'/refreshToken',
passport.authenticate('onlyForRefreshToken', { session: false }),
authenticationCtrl.riderRefreshToken,
);
In conclusion, when JWT passes from jwtstrategy without problem, that Post router would work.
And I want to use jwtPayload that is in jwtstrategy into Nodejs API as req.params or req.body.
Could you help me this problem?
You need to wrap your strategy into a function that gets req and res :
const isAuthenticated: RequestHandler = (req, res, next) => {
passport.authenticate(
'jwt',
{ session: false, failWithError: true },
(error, user) => {
if (error) {
return next(error)
}
//HERE PUT USER WHERE YOU WANT
//req.data or req.user or req.userInfo
req.user = user
return next()
}
)(req, res, next)
}
I don't recommend putting the user into req.params nor req.body since it might be confusing later on (because technically it doesn't come from those).
I'm trying to use koa-passport for koa2, and followed the examples of the author, but i always get "Unauthorized". I used the console.log and found that it even not hit the serializeUser.
var UserLogin = async (ctx, next) =>{
return passport.authenticate('local', function(err, user, info, status) {
if (user === false) {
ctx.body = { success: false }
} else {
ctx.body = { success: true }
return ctx.login(user)
}
})(ctx, next);
};
And then I searched on the web and found another writing of router, it goes to the serializeUser but the done(null, user.id) threw error that "cannot get id from undefined".
let middleware = passport.authenticate('local', async(user, info) => {
if (user === false) {
ctx.status = 401;
} else {
await ctx.login(ctx.user, function(err){
console.log("Error:\n- " + err);
})
ctx.body = { user: user }
}
});
await middleware.call(this, ctx, next)
The auth.js are showed below. Also I followed koa-passport example from the author here and tried to use session, but every request i sent will get a TypeError said "Cannot read property 'message' of undefined". But I think this is not the core problem of authentication, but for reference if that really is.
const passport = require('koa-passport')
const fetchUser = (() => {
const user = { id: 1, username: 'name', password: 'pass', isAdmin: 'false' };
return async function() {
return user
}
})()
const LocalStrategy = require('passport-local').Strategy
passport.use(new LocalStrategy(function(username, password, done) {
fetchUser()
.then(user => {
if (username === user.username && password === user.password) {
done(null, user)
} else {
done(null, false)
}
})
.catch(err => done(err))
}))
passport.serializeUser(function(user, done) {
done(null, user.id)
})
passport.deserializeUser(async function(id, done) {
try {
const user = await fetchUser();
done(null, user)
} catch(err) {
done(err)
}
})
module.exports = passport;
By the way when I use the simple default one, it will just give me a "Not found". But through console.log I can see it actually got into the loginPass.
var loginPass = async (ctx, next) =>{
passport.authenticate('local', {
successRedirect: '/myApp',
failureRedirect: '/'
});
};
In server.js:
// Sessions
const convert = require('koa-convert');
const session = require('koa-generic-session');
app.keys = ['mySecret'];
app.use(convert(session()));
// authentication
passport = require('./auth');
app.use(passport.initialize());
app.use(passport.session());
Thanks a lot for any help!!! :D