Any web application of significant complexity is going to require user-level security. In this post, I walk through how to create authorization for users in a MERN (MongoDB, Express, React, and Node) stack web application.
Full code is available here: https://github.com/dikuw/mern-passport
This post assumes knowledge of React and node.js and just shows how to incorporate routes for authorization using Passport.js.
First I’ll start with the server routes. I grouped everything in to one controller, although this could probably be cleanly split between separate User and Authorization controllers. The router only has four (4) routes:
router.get('/', authController.getCurrentUser);
router.post('/register', authController.checkAlreadyRegistered, authController.registerUser, passport.authenticate('local'), authController.login);
router.post('/login', passport.authenticate('local'), authController.login);
router.post('/logout', authController.logoutUser);
router.get('/')
is the route is that is fired on initial app load and any home page reload. getCurrentUser
looks like this:
exports.getCurrentUser = (req, res) => {
console.log('current user: ', req.user);
if (req.user) {
res.json({ user: req.user });
} else {
res.json({ user: null });
};
};
All this is doing is checking if there is a user on the route. If so, the route returns the user so the front end can set the state. It looks like this:
getUser = () => {
axios.get('/user/').then(response => {
if (response.data.user) {
this.setState({
isLoggedIn: true,
username: response.data.user.username
});
} else {
this.setState({
isLoggedIn: false,
username: null
});
}
});
}
req.user
is persisted in a passport session, which is why this works any time “/” is revisited.
Next we have router.post('/register')
which is the most complex route with four distinct functions. Let’s look at each in turn.
authController.checkAlreadyRegistered
verified the username is not already in the database. It looks like this:
exports.checkAlreadyRegistered = async (req, res, next) => {
const { username } = req.body;
const registered = await User.find({ username });
if (registered[0] && registered[0]._id) {
res.json({ error: `Sorry, already a user with the username: ${username}` });
return;
}
next();
}
If the user is not already registered, the route continues to authController.registerUser
to create the new user in the database, which looks like
exports.registerUser = async (req, res, next) => {
const { username, password } = req.body;
await (new User({ username, password })).save();
next();
};
Then we run passport.authenticate('local')
to create the session. More on this below. And finally we log in the newly registered user with authController.login
:
exports.login = (req, res) => {
req.login(req.user, function(err) {
if (err) { res.json({ error: err }); }
return res.send(req.user);
});
};
req.login()
is one of the functions that Passport.js exposes on the request object. Another one is req.logout
, which we’ll see below.
On the front end, we handle the req.user object as follows:
login = async () => {
const { username, password } = this.state;
let response = await axios.post('/user/login/', { username, password });
if (response.status === 200) {
this.setState({ isLoggedIn: true });
this.props.history.push("/");
} else {
console.log('login error');
}
}
The final route is router.post('/logout')
which is one simple function, using another passport.js method:
exports.logout = (req, res) => {
if (req.user) {
req.logout();
res.send({ msg: 'logging out' });
} else {
res.send({ msg: 'no user to log out' })
};
};
Logout is handled on the front end as so:
logout = () => {
axios.post('/user/logout').then(response => {
if (response.status === 200) {
this.setState({
isLoggedIn: false,
username: null,
password: null
});
this.props.history.push("/");
}
}).catch(error => {
console.log('logout error', error);
});
}
This code can be improved to alert the user when certain calls fail (for instance, on checkAlreadyRegistered or if you use the wrong password.
Any other changes?