Node.js Passport and Terms of Use: sign in to MyPortfolio
In my application named MyPortfolio (built with Node.js/Express and MongoDB), I decided to use Passport as authentication middleware.
It is extremely flexible and modular, and it comes with a comprehensive set of strategies which support authentication using a username and password, Google-OAuth, Facebook, Twitter, and more.
I started developing a Passport authentication, by following a very interesting tutorial of Scotch.io, where it is clearly illustrated how to use Passport to perform local authentication, Facebook, Twitter, Google authentications, and even how to link all social accounts under one user account.
But there was something lacking in that tutorial, and many other Passport tutorials I found on the web: how could I use Passport middleware to perform a sign-up only after user viewed and accepted my application’s Terms of Use?
Let me show you the architecture, the models and the operations flow I used (check also the code here MyPortfolio code).
Package.json
Here are the dependencies I’m using in package.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// package.json
...
"dependencies": {
"async": "^2.5.0",
"bcrypt-nodejs": "0.0.3",
"body-parser": "^1.18.2",
"connect-flash": "^0.1.1",
"cookie-parser": "^1.4.3",
"dotenv": "^4.0.0",
"ejs": "^2.5.7",
"express": "^4.14.0",
"express-ejs-layouts": "^2.3.1",
"express-session": "^1.15.6",
"express-validator": "^4.2.1",
"fs-extra": "^4.0.2",
"mongoose": "^4.11.13",
"multer": "^1.3.0",
"passport": "^0.4.0",
"passport-facebook": "^2.1.1",
"passport-google-oauth": "^1.0.0",
"passport-local": "^1.0.0",
"passport-twitter": "^1.0.4",
"summernote-nodejs": "^1.0.4"
}
...
Because of I decided to let users log in only with their Google accounts in MyPortfolio, the dependencies of interest in this post are:
express is the framework.
express-session to handle the session in Express framework.
ejs is the templating engine.
mongoose is the object modeling for our MongoDB database.
passport and passport-google-oauth for Google authentication.
connect-flash allows to pass session flashdata messages.
fs-extra empowers Node.js fs library with Promises.
User model
Here is the user model schema, which resembles the one in the Scotch.io tutorial.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// user.js
...
var userSchema = mongoose.Schema({
local : {
email : String,
password : String,
},
facebook : {
id : String,
token : String,
email : String,
name : String
},
twitter : {
id : String,
token : String,
displayName : String,
username : String
},
google : {
id : String,
token : String,
email : {type: String, index: true},
name : {type: String, index: true},
imageUrl : String,
eslug : String
},
mustacceptterms : {
type: Boolean,
default: 'false'
}
});
...
You can notice that in the userSchema I added another boolean parameter, named mustacceptterms: this one will be true until the user will have explicitly confirmed to have read and accepted the Terms of Use.
Application setup
server.js contains the setup of the different packages, included Express, Passport, Session and Mongoose
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// server.js
// load environment variables
require('dotenv').config();
// grab our dependencies
const express = require('express'),
app = express(),
port = process.env.PORT || 8080
expressLayouts = require('express-ejs-layouts'),
mongoose = require('mongoose'),
passport = require('passport'),
bodyParser = require('body-parser'),
session = require('express-session'),
cookieParser = require('cookie-parser'),
flash = require('connect-flash'),
expressValidator = require('express-validator');
// configure our application
// set sessions and cookie parser
app.use(cookieParser());
app.use(session({
secret: process.env.SECRET,
cookie: {maxAge: 8*60*60*1000}, // 8 hours
resave: false, // forces the session to be saved back to the store
saveUninitialized: false// don't save unmodified sessions
// if there is a user id already but no token (user was linked at one point and then removed)
if (!user.google.token) {
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
if(!user.mustacceptterms) {
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
else {
return done(null, user);
}
}
// brand new user
else {
var newUser = new User();
newUser.google.id = profile.id;
newUser.google.token = token;
newUser.google.name = profile.displayName;
newUser.google.email = profile.emails[0].value; // pull the first email
newUser.google.imageUrl = profile.photos[0].value; // pull the first image
newUser.mustacceptterms = true; // the new user must accept terms of use before creating the account on MyPortfolio
newUser.save(function(err) {
if (err) {
throw err;
}
return done(null, newUser);
});
}
});
}
else {
// user already exists and is logged in, we have to link accounts
var user = req.user; // pull the user out of the session
user.google.id = profile.id;
user.google.token = token;
user.google.name = profile.displayName;
user.google.email = profile.emails[0].value; // pull the first email
user.google.imageUrl = profile.photos[0].value; // pull the first image
initUserFolderStructure(profile.id, () => {
user.save(function(err) {
if (err) {
throw err;
}
return done(null, user);
});
});
}
}));
If a user logs in to MyPortfolio for the first time (‘A brand new user’), he will be saved to the database with the flag mustacceptterms set to true, and no folder structure will be created.
Even if the user had previously logged in without accepting the Terms of Use, he would just be considered authenticated, but the flag mustacceptterms would remain set to true.
So I added two different functions to determine if user is really logged-in, or if he’s just authenticated, and the flag mustacceptterms is the determiner.
Handling routing
The routes.js contains the authentication logic (and much more):
The utility function isLoggedIn determines if a user is logged in the application.
The utility function isLoggedInToAcceptTerms determines if a user is just authenticated (but still never accepted the Terms of Use).
The route ‘/auth/google’ uses passport authentication method (which was previously configured with the Google Oauth2 Strategy) to authenticate the user with his Google account.
The magic happens for the route ‘/auth/google/callback’, which represents the callback invoked after the Google authentication procedure. I used a ‘failureRedirect’ to the home page when the authentication fails, and a Custom function on correct Google authentication.
Thanks to this custom function I can determine whether the Terms of Use haven’t been previously accepted, and in that case redirect the user to the Terms of Use page: from there the user will have the chance to read and accept the Terms and complete the sign-in process.