Live Site URL: secrets-basuabhirup.herokuapp.com
This project is a part of "The Complete 2021 Web Development Bootcamp" by The London App Brewery, instructed by Dr. Angela Yu and hosted on Udemy.
- Objective of this Project
- Steps I have followed
- Basic Server Setup
- Experiment with 6 different levels of security
- Level 1 Security: Login with registered Username and Password
- Level 2 Security: Database Encryption
- Level 3 Security: Hashing passwords with MD5
- Level 4 Security: Salting and Hashing passwords with bcrypt
- Level 5 Security: Add Cookies and Sessions using Passport.js
- Level 6 Security: Implementing Third Party Sign-in using OAuth 2.0
- Finishing Up the App
- To create an app where the registered users can share secrets anonymously.
- To restrict access to the secrets by all the non-registered users.
- To add authentication to the app so that individual users would sign up with a username and password.
- To enable users to log into the app using their username and password to access to the secrets.
- Initialized NPM inside the project directory using
npm init -y
command from the terminal. - Used
npm i express body-parser ejs mongoose dotenv
command to install these dependencies for our project. - Initialized the server with the following code inside the
app.js
file:
// Require necessary NPM modules:
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const ejs = require('ejs');
const mongoose = require('mongoose');
// Assign a port for localhost as well as final deployement:
const port = process.env.PORT || 3000;
// Initial setup for the server:
const app = express();
app.use(express.static('public'));
app.set('view engine', 'ejs');
app.use(bodyParser.urlencoded({extended: true}));
// Connect to a new MongoDB Database, using Mongoose ODM:
// Create a new collection inside the database to store data:
// Handle HTTP requests:
// Handle 'GET' requests made on the '/' route:
app.get('/', (req, res) => {
res.render('home');
})
// Handle 'GET' requests made on the '/register' route:
app.get('/register', (req, res) => {
res.render('register');
})
// Handle 'GET' requests made on the '/login' route:
app.get('/login', (req, res) => {
res.render('login');
})
// Enable client to listen to the appropriate port:
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
- Initialized a local git repository with
git init
command. - Created a
.gitignore
file to prevent all the unwanted files from being tracked:
node_modules/
npm-debug.log
.DS_Store
.env
- Created a
.env
file to store all the encryption keys / database passwords as environment variables and made sure that the.env
file is not being tracked by git:
DB_USER=my_user_name
DB_PASS=Abc123xyz789
- Connected the server to a new database named
userDB
in MongoDB Atlas Cluster, using Mongoose ODM:
// Connect to a new MongoDB Database, using Mongoose ODM:
mongoose.connect(`mongodb+srv://${process.env.DB_USER}:${process.env.DB_PASS}@cluster0.m5s9h.mongodb.net/userDB`);
- Created a new collection named
users
insideuserDB
to store the emails and passwords of all the users:
// Create a new collection named 'users' to store the emails and passwords of the users:
const userSchema = new mongoose.Schema({
email: String,
password: String
})
const User = new mongoose.model('User', userSchema);
- Handled the HTTP
POST
requests made on the/register
route, so that it creates a new user document in the database to store their emails and passwords:
app.post('/register', (req, res) => {
const user = new User({
email: req.body.username,
password: req.body.password
})
user.save(err => {
if(err) {
console.log(err);
} else {
res.render('secrets');
}
})
})
- Handled the HTTP
POST
requests made on the/login
route, so that the registered users can seamlessly login to the app with their credentials:
app.post('/login', (req, res) => {
User.findOne({email: req.body.username}, (err, user) => {
if(err) {
res.send(err);
} else {
if(user) {
if(user.password === req.body.password) {
res.render('secrets');
} else {
res.send("Please check your password and try again...");
}
} else {
res.send("Please check your username and try again...");
}
}
})
})
- Installed the
mongoose-encryption
NPM module usingnpm i mongoose-encryption
command and required the same inside ourapp.js
file:
const encrypt = require('mongoose-encryption');
- Created a long unguessable secret key as an environment variable inside the
.env
file and added an encryption package to ouruserSchema
to keep the users' passwords encrypted:
// Add the encryption package to our userSchema before creating the User model:
let secret = process.env.SECRET_STRING;
userSchema.plugin(encrypt, { secret: secret, encryptedFields: ['password'] });
mongoose-encryption
package is smart enough to decrypt the passwords from the database whenever POST requests are created on the/login
route. Therefore, anyone having access to the server code inapp.js
file can get access to all the users' passwords.
- Installed the
md5
module usingnpm i md5
command and required the same in ourapp.js
file:
const md5 = require('md5');
- Modified the user object inside the handler function of the
POST
requests made on the/register
route, so that instead of storing the users' passwords in our database, we use our Hash function MD5 to turn that into an irreversible hash.
const user = new User({
email: req.body.username,
password: md5(req.body.password)
})
- Modified the logic inside the handler function of the
POST
requests made on the/login
route:
if(user.password === md5(req.body.password)) {
res.render('secrets');
}
- Installed the
bcrypt
module usingnpm i bcrypt
command and required the same in ourapp.js
file:
const bcrypt = require('bcrypt');
- Modified handler of
POST
requests made on the/register
route, to turn the users' passwords into a complex hash using 10 rounds of salting:
app.post('/register', (req, res) => {
bcrypt.hash(req.body.password, 10, (err, hash) => {
const user = new User({
email: req.body.username,
password: hash
})
user.save(err => {
if(err) {
console.log(err);
} else {
res.render('secrets');
}
})
})
})
- Modified handler of
POST
requests made on the/login
route:
if(user) {
bcrypt.compare(req.body.password, user.password, (err, result) => {
if(result === true) {
res.render('secrets');
} else {
res.send("Please check your password and try again...");
}
})
} else {
res.send("Please check your username and try again...");
}
- Installed the
passport
,passport-local
,passport-local-mongoose
andexpress-session
modules usingnpm i passport passport-local passport-local-mongoose express-session
command from terminal and required them inside ourapp.js
file, maintaining order as following:
const session = require('express-session');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const passportLocalMongoose = require('passport-local-mongoose');
- Created a
SECRET_KEY
as an environment variable inside the.env
file, configuredsession
below all theapp.use
commands and initializedpassport
right below this:
app.use(session({
secret: process.env.SECRET_KEY,
resave: false,
saveUninitialized: false
}))
app.use(passport.initialize());
app.use(passport.session());
- Added
passportLocalMongoose
plugin to theuserSchema
before creating the User model, so that it can salt and hash the users' passwords before saving them to the database:
userSchema.plugin(passportLocalMongoose);
- Added
passport-local
configurations right below the User model:
// Add passport-local Configuration:
passport.use(new LocalStrategy(User.authenticate()));
passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());
- Handled the HTTP
POST
requests made on the/register
route, using some methods from thepassport
and thepassportLocalMongoose
packages:
app.post('/register', (req, res) => {
User.register({username: req.body.username}, req.body.password, (err, user) => {
if(err){
res.send(`
<h3 style="font-family:sans-serif; color:red;">${err.message}</h3>
<button type="button" style="font-size:1rem; cursor:pointer;" onclick="window.location.href='/register'">Go back to Registration Page</buton>
`);
} else {
passport.authenticate('local')(req, res, () => {
res.redirect('/secrets');
})
}
})
})
- Handled the HTTP
POST
requests made on the/login
route, using some methods from thepassport
and thepassportLocalMongoose
packages:
app.post('/login', passport.authenticate('local', {
successRedirect: '/secrets',
failureRedirect: '/login',
}));
- Handled the HTTP
GET
requests made on the/secrets
route, so that only the logged in authenticated users can get access to the secrets:
app.get('/secrets', (req, res) => {
if(req.isAuthenticated()) {
res.render('secrets');
} else {
res.redirect('/login');
}
})
- Handled the HTTP 'GET' requests made on the '/logout' route to deauthenticate the user and end the user session, using the
req.logout()
method from thepassport
module:
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/');
})
- Installed the
passport-google-oauth20
andmongoose-findorcreate
modules usingnpm i passport-google-oauth20 mongoose-findorcreate
command and required them inside ourapp.js
file:
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const findOrCreate = require('mongoose-findorcreate');
- Created a new project in the Google Developers Console, generated OAuth 2.0 client ID with source URI as
http://localhost:3000
, redirect URI ashttp://localhost:3000/auth/google/secrets
and stored the credentials in our.env
file as environment variablesGOOGLE_CLIENT_ID
andGOOGLE_CLIENT_SECRET
. - Added the
findOrCreate
function as a plugin to theuserSchema
before creating the User model:
userSchema.plugin(findOrCreate);
- Configured
passport-google-OAuth20
strategy:
// Add passport-google-OAuth20 strategy configuration:
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/secrets',
},
function(accessToken, refreshToken, profile, cb) {
const id = profile.id;
const userEmail = profile.emails[0].value;
User.findOrCreate({ username: userEmail, googleId: id },
(err, user) => {
return cb(err, user);
})
}
))
- Added a
googleId
field to theuserSchema
:
const userSchema = new mongoose.Schema({
username: String,
password: String,
googleId: String
})
- Created Google 'Sign Up' and 'Sign In' buttons to make
GET
requests on the/auth/google
route: - Handled
GET
requests made on the/auth/google
route:
app.get('/auth/google', passport.authenticate('google', {scope: ['profile', 'email'] }));
- Handled
GET
requests made on the authorized redirect route/auth/google/secrets
:
app.get('/auth/google/secrets',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
res.redirect('/secrets');
});
- Modified passport-local configurations and updated the methods of serialize and deserialize users to make them compatible with all OAuth strategies:
passport.serializeUser(function(user, done) {
done(null, user.id);
});
passport.deserializeUser(function(id, done) {
User.findById(id, function(err, user) {
done(err, user);
});
});
- Modified styles of the login button using bootstrap-social stylesheets.
- Similary, created Facebook Sign-in authentication using
passport-facebook
module, with the help of its official documentation page. Created a Test App in Facebook Developers Console to test the authentication. - Fixed the back button issue by adding the following code to the handler function of the
GET
requests made on the/secrets
route:
res.set('Cache-Control', 'no-store');
- Created a new 'secrets' field inside our
userSchema
to store the secrets.
const userSchema = new mongoose.Schema({
username: String,
password: String,
googleId: String,
facebookId: String,
secrets: [{secret: String}]
})
- Handled
GET
requests made on the/submit
route:
app.get('/submit', (req, res) => {
res.set('Cache-Control', 'no-store');
if(req.isAuthenticated()) {
res.render('submit');
} else {
res.redirect('/login');
}
})
- Handled
POST
requests made on the/submit
route:
app.post('/submit',(req, res) => {
User.findOne({ _id: req.user._id}, (err, user) => {
if(err) {
console.log(err);
} else {
if(user) {
user.secrets.push({secret: req.body.secret});
user.save(() => {
res.redirect('/secrets')
});
}
}
})
})
- Updated the template code inside
secrets.ejs
file:
<% users.forEach(user => { %>
<% user.secrets.forEach(secret => { %>
<p class="secret-text"><%= secret.secret %></p>
<% }) %>
<% }) %>
- Modified handler of
GET
requests made on the/secrets
route:
app.get('/secrets', (req, res) => {
res.set('Cache-Control', 'no-store');
if(req.isAuthenticated()) {
User.find({'secret': {$ne: null}}, (err, users) => {
if(err) {
console.log(err);
} else {
if (users) {
res.render('secrets', {users: users});
}
}
})
} else {
res.redirect('/login');
}
})
- Finally, created a
Procfile
, replaced alllocalhost:3000
links with the live deployment URIs in both the server code as well as in Google and Facebook's Developers Console, and made all other necessary changes to prepare the codebase for deployment via Heroku server.