Authorization and Flash Messages
We'll need to...
- Include middleware for display flash messages on webpages
- Create and include middleware for accessing the current user and flash messages.
- Create and include authorization middleware to limit access to webpages
Flash messages via connect-flash
What are flash messages?
Flash messages are temporary (one-shot) messages used to display an error or info to the user. The messages are typically stored in the session, which allows us to redirect the user to a new page and display a message after redirect. Once the user refreshes or redirects away from the page, the message will disappear from the session.
In Express, we use a middleware called connect-flash to handle flash messages.
connect-flash requires session
, so you must load the express-session middleware first if you want to pass flash messages between pages.
Installing connect-flash
npm install --save connect-flash
Including connect-flash
index.js
// require connect-flash at the top of the page
var flash = require('connect-flash');
/*
* Include the flash module by calling it within app.use().
* IMPORTANT: This MUST go after the session module
*/
app.use(flash());
Sending flash messages
Flash messages can be sent by calling req.flash()
within a route, and passing along a key and value. We can also configure Passport to automatically add success and failure flash messages. Let's include the following flash messages in your code, to replace your console.log
statements. Look for the // FLASH
comments to see where you should add flash calls.
controllers/auth.js
var express = require('express');
var db = require('../models');
var passport = require('../config/ppConfig');
var router = express.Router();
router.get('/signup', function(req, res) {
res.render('auth/signup');
});
router.post('/signup', function(req, res) {
User.create({
email: req.body.email,
name: req.body.name,
password: req.body.password
}, function(err, createdUser) {
if(err){
// FLASH -
req.flash('error', 'Could not create user account');
res.redirect('/auth/signup');
} else {
// FLASH
passport.authenticate('local', {
successRedirect: '/',
successFlash: 'Account created and logged in'
})(req, res);
}
});
});
router.get('/login', function(req, res) {
res.render('auth/login');
});
// FLASH
router.post('/login', passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/auth/login',
failureFlash: 'Invalid username and/or password',
successFlash: 'You have logged in'
}));
router.get('/logout', function(req, res) {
req.logout();
// FLASH
req.flash('success', 'You have logged out');
res.redirect('/');
});
module.exports = router;
Retrieve the messages in the view
We can retrieve the message using req.flash()
and pass that message in to the view. However, we don't want to remember to do that on every route, like this:
//Don't do this
app.get('/', function(req, res) {
res.render('index', { alerts: req.flash() });
});
Instead, we can create middleware to make the messages accessible in every view. This can be done by attaching the value to res.locals
. While we're at it, let's add the currently logged in user as well. Add the following middleware after the rest of your middleware.
app.use(function(req, res, next) {
// before every route, attach the flash messages and current user to res.locals
res.locals.alerts = req.flash();
res.locals.currentUser = req.user;
next();
});
Display messages and current user
Once we pass the alerts through to the view, we can display them by accessing the object as alerts
.
In order to display the alerts, we can make a partial that renders on every page.
views/partials/alerts.ejs
<% if (alerts.error) { %>
<% alerts.error.forEach(function(msg) { %>
<div class="alert alert-danger"><%= msg %></div>
<% }); %>
<% } %>
<% if (alerts.success) { %>
<% alerts.success.forEach(function(msg) { %>
<div class="alert alert-success"><%= msg %></div>
<% }); %>
<% } %>
Rendering the partial and the current user:
views/layout.ejs
<%- JSON.stringify(currentUser) %>
<% include partials/alerts %>
Authorization for web pages
Lastly, let's add authorization so users need to be logged in to access certain pages. Let's create the middleware for authorization in a separate file within the middleware folder.
middleware/isLoggedIn.js
module.exports = function(req, res, next) {
if (!req.user) {
req.flash('error', 'You must be logged in to access that page');
res.redirect('/auth/login');
} else {
next();
}
};
Whenever we want to limit access to a particular page, require this middleware on the route. The current logic will redirect the user to the login route if they're not logged in.
index.js
// require the authorization middleware at the top of the page
var isLoggedIn = require('./middleware/isLoggedIn');
app.get('/profile', isLoggedIn, function(req, res) {
res.render('profile');
});
Alternatively, you can keep all protected routes below the LoggedIn Middleware.
index.js
// require the authorization middleware at the top of the page
var isLoggedIn = require('./middleware/isLoggedIn');
app.use(isLoggedIn)
// anything below here requires the user to be logged in
app.get('/profile', function(req, res) {
res.render('profile');
});
This should pass the following tests
GET /profile - should redirect to /auth/login if not logged in
GET /profile - should return a 200 response if logged in
Conclusion and CSRF
Congrats, you have a working application with user authentication and authorization! To ensure all components are working, run npm test
to verify all tests pass.
See the finished OAuth example here:
https://github.com/wdi-sg/express-authentication-mongoose/tree/solution
Additional: Cross-Site Request Forgeries
Something we did not cover in this walk-through, which is part of the OWASP Top 10 Web Application Security Flaws is Cross-Site Request Forgeries (CSRF).
CSRF is a security flaw where a third-party site attempts to make a request to your site with your site's session cookie. For example, if I'm logged into Reddit, I could come across a link that looks like this:
<a href="http://www.reddit.com/accounts.php?action=delete">Delete account. Are you sure?</a>
But what if I was in a comment section, and someone decided to disguise this link?
<a href="http://www.reddit.com/accounts.php?action=delete">Free subscription!</a>
This would delete my account! To prevent this forgery, we can generate a unique token on the server that must be sent with this delete request, then verify the token on the server.
<a href="http://www.reddit.com/accounts.php?action=delete&csrf_token=aGIe3Sl8a9FlsdLkJVZ">Free subscription!</a>
For time's sake, we won't be implementing this together. However, there's an Express module called csurf that can be used to protect against CSRF attacks via tokens. Feel free to look at the examples and implement this on your own. When working in Rails, this functionality will be included automatically.
Another note, this protection does not apply to APIs. APIs should use a key instead.