Node.js series Part 4. Express Website with authentication and authorization in a Mac Production Environment
In a real production environment the app runs as a service in the background and this service is managed by a process manager. And the app should run behind a reverse proxy server. This reverse proxy server manage the TLS encryption, receives the requests from the client and route any request to the app running in the background. So the connection from the client to the reverse proxy server is TLS encrypted. Therefore the data transferred between client and the reverse proxy are secured.
How to setup such a production environment will be shown in the first chapter of this documentation.
The express app itself also contains some security features. The app contains a session based user authentication and HTTP headers which help to further secure the app. This is explained of the second chapter of this documentation.
Finally the express app should use the template engine PUG to render the HTML for us. This is describes of the third chapter of this documentation.
1. Setup the production environment
Installation of mongodb
The command brew tap
without any arguments lists the GitHub repositories that are currently linked to your Homebrew installation.
Patricks-MBP:~ patrick$ brew tap
homebrew/cask
homebrew/core
homebrew/services
The formula mongodb has been removed from homebrew-core. But fortunately the MongoDB Team is maintaining a custom Homebrew tap on GitHub. Read the instructions in the README.md file.
Add the custom tap in the Mac OS terminal and install mongodb.
Patricks-MBP:~ patrick$ brew tap mongodb/brew
Patricks-MBP:~ patrick$ brew tap
homebrew/cask
homebrew/core
homebrew/services
mongodb/brew
Patricks-MBP:~ patrick$ brew install mongodb-community@4.2
After the installation the relevant paths are.
the configuration file (/usr/local/etc/mongod.conf)
the log directory path (/usr/local/var/log/mongodb)
the data directory path (/usr/local/var/mongodb)
Check the services with homebrew
brew services list
Name Status User Plist
mongodb-community started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.mongodb-community.plist
Start and stop mongodb.
brew services start mongodb-community
brew services stop mongodb-community
Setup mongodb for the project
Setup an admin user
:# mongo
> use admin
switched to db admin
> db
admin
> db.createUser({ user: "adminUser", pwd: "adminpassword", roles: [{ role: "userAdminAnyDatabase", db: "admin" }, {"role" : "readWriteAnyDatabase", "db" : "admin"}] })
> db.auth("adminUser", "adminpassword")
1
> show users
{
"_id" : "admin.adminUser",
"userId" : UUID("5cbe2fc4-1e54-4c2d-89d1-317340429571"),
"user" : "adminUser",
"db" : "admin",
"roles" : [
{
"role" : "userAdminAnyDatabase",
"db" : "admin"
},
{
"role" : "readWriteAnyDatabase",
"db" : "admin"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
> exit
Enable authentication with security: authorization: enabled
#> nano /usr/local/etc/mongod.conf
systemLog:
destination: file
path: /usr/local/var/log/mongodb/mongo.log
logAppend: true
storage:
dbPath: /usr/local/var/mongodb
net:
port: 27017
bindIp: 127.0.0.1
security:
authorization: enabled
Login and authenticate with admin
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b3e7f48a-a05c-4894-87db-996cb34eb1fb") }
MongoDB server version: 4.2.3
> show dbs
> db
test
> use admin
switched to db admin
> db
admin
> show dbs
> db.auth("adminUser", "adminpassword")
1
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
If you login you dont see any databases when you call show dbs
. The default database you are connected to is test
.
Then you connect to admin database. For admin you setup the admin user with the roles userAdminAnyDatabase
and readWriteAnyDatabase
. With these permissions the admin user can manage users for any database and has read and write access to any database.
So wehen you logon to admin database with the admin user you are able to see all databases with show dbs
.
Mongodb comes with 3 standard dbs pre installed:
- admin
- config
- local
Create a new database for our express-security app (authenticated as admin user – see above)
> use express-security
switched to db express-security
> db
express-security
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
>
The DB which you’ve created is not listed here. We need to insert at least one collection into it for displaying that database in the list.
> db
express-security
> db.createCollection("col_default")
{ "ok" : 1 }
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> exit
Create an owner user for express-security database using the admin user
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("79f79b63-9d08-489f-9e6c-bfc10d8cc09e") }
MongoDB server version: 4.2.3
> db
test
> show dbs
> use admin
switched to db admin
> db.auth("adminUser", "adminpassword")
1
> db
admin
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> use express-security
switched to db express-security
> db.createUser({ user: "owner_express-security", pwd: "passowrd", roles: [{ role: "dbOwner", db: "express-security" }] })
Successfully added user: {
"user" : "owner_express-security",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security"
}
]
}
> db
express-security
> show users
{
"_id" : "express-security.owner_express-security",
"userId" : UUID("7a0bafb2-d2ed-4d18-9aba-e2f15a503ec5"),
"user" : "owner_express-security",
"db" : "express-security",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security"
}
],
"mechanisms" : [
"SCRAM-SHA-1",
"SCRAM-SHA-256"
]
}
> exit
Connection string to connect to express-security db using the owner_express-security user:
mongodb://owner_express-security:password@localhost/express-security
Installation of PM2
PM2 is a process manager for Node.js applications. It can daemonize applications to run them as a service in the background.
I install pm2 as a global npm package on my Mac.
Patricks-Macbook Pro:~ patrick$ npm install pm2 -g
Then navigate to your project directory.
Patricks-Macbook Pro:~ patrick$ cd Software/dev/node/articles/2020-05-15-express-security/express-security
Patricks-Macbook Pro:~ patrick$ ls -l
total 112
drwxr-xr-x 5 patrick staff 160 30 Mai 05:28 database
drwxr-xr-x 115 patrick staff 3680 30 Mai 19:35 node_modules
-rw-r--r-- 1 patrick staff 34366 30 Mai 19:35 package-lock.json
-rw-r--r-- 1 patrick staff 339 30 Mai 19:35 package.json
-rw-r--r--@ 1 patrick staff 12343 30 Jun 05:00 secserver.js
drwxr-xr-x 3 patrick staff 96 30 Mai 05:03 static
Patricks-Macbook Pro:express-security patrick$
Start your app using pm2
Patricks-Macbook Pro:express-security patrick$ pm2 start secserver.js
Patricks-Macbook Pro:~ patrick$ pm2 list
┌─────┬──────────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ secserver │ default │ 1.0.0 │ fork │ 640 │ 16h │ 0 │ online │ 0% │ 48.6mb │ patrick │ disabled │
└─────┴──────────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
Patricks-Macbook Pro:~ patrick$
Other comands to control the process manager.
pm2 start secserver.js
pm2 start <id>
pm2 list
pm2 stop <id>
pm2 restart <id>
pm2 show <id>
Installation of nginx
nginx is an open source HTTP and an HTTP Reverse Proxy Server (also mail proxy and load balancer etc.). I install nginx on my Mac with Homebrew.
brew install nginx
You can list the brew services with the following command.
Patricks-MBP:digitaldocblog-V3 patrick$ brew services list
Name Status User Plist
mongodb-community started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.mongodb-community.plist
nginx started patrick /Users/patrick/Library/LaunchAgents/homebrew.mxcl.nginx.plist
Patricks-MBP:digitaldocblog-V3 patrick$
You can start and stop the brew services as follows.
brew install nginx
brew services start nginx
brew services stop nginx
Setup nginx with TLS/SSL
SSL/TLS works by using the combination of a public certificate and a private key.
The SSL key (private key) is kept secret on the server. It is used to encrypt content sent to clients.
The SSL certificate is publicly shared with anyone requesting the content. It can be used to decrypt the content signed by the associated SSL key.
create private key
Patricks-MBP:express-security patrick$ cd /usr/local/etc/nginx
Patricks-MBP:nginx patrick$ ls -l
total 144
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf.default
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params.default
-rw-r--r-- 1 patrick admin 2837 5 Apr 13:18 koi-utf
-rw-r--r-- 1 patrick admin 2223 5 Apr 13:18 koi-win
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types.default
-rw-r--r-- 1 patrick admin 3106 15 Mai 05:19 nginx.conf
-rw-r--r-- 1 patrick admin 2680 5 Apr 13:18 nginx.conf.default
-rw-r--r-- 1 patrick admin 3091 21 Jan 05:40 nginx.conf.working
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params.default
drwxr-xr-x 3 patrick admin 96 21 Jan 06:02 servers
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params.default
-rw-r--r-- 1 patrick admin 3610 5 Apr 13:18 win-utf
Patricks-MBP:nginx patrick$ mkdir ssl
Patricks-MBP:nginx patrick$ ls -l
total 152
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf
-rw-r--r-- 1 patrick admin 1077 5 Apr 13:18 fastcgi.conf.default
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params
-rw-r--r-- 1 patrick admin 1007 5 Apr 13:18 fastcgi_params.default
-rw-r--r-- 1 patrick admin 2837 5 Apr 13:18 koi-utf
-rw-r--r-- 1 patrick admin 2223 5 Apr 13:18 koi-win
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types
-rw-r--r-- 1 patrick admin 5231 5 Apr 13:18 mime.types.default
-rw-r--r--@ 1 patrick admin 373 18 Mai 05:38 nginx.conf
-rw-r--r-- 1 patrick admin 2680 5 Apr 13:18 nginx.conf.default
-rw-r--r-- 1 patrick admin 3091 21 Jan 05:40 nginx.conf.working
-rw-r--r--@ 1 patrick admin 1390 17 Mai 05:19 nginx_old.conf
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params
-rw-r--r-- 1 patrick admin 636 5 Apr 13:18 scgi_params.default
drwxr-xr-x 5 patrick admin 160 18 Mai 05:20 servers
drwxr-xr-x 4 patrick admin 128 16 Mai 05:41 ssl
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params
-rw-r--r-- 1 patrick admin 664 5 Apr 13:18 uwsgi_params.default
-rw-r--r-- 1 patrick admin 3610 5 Apr 13:18 win-utf
Patricks-MBP:nginx patrick$ cd ssl
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl genrsa -out privateKey.pem 4096
Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
create certificate signing request (CSR)
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl req -new -key privateKey.pem -out csr.pem
Patricks-MBP:ssl patrick$ ls -l
total 16
-rw-r--r-- 1 patrick admin 1740 16 Mai 05:23 csr.pem
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
In case I would like to request an official certificate I must send this csr to the certificate authority. This authority would then create an authority signed certificate from the CSR and send it back to me.
This step is done by ourselves and this is the reason why we create a self signed certificate. This self signed certificate is not a official certificae and not trusted by any browser. It is not useful to use a self signed certificate in production because it produces error messages in the browsers. But for local development a self signed certificate is ok.
So create the self signed certificale. The csr file can then be removed.
create the self signed certificate
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl x509 -in csr.pem -out selfsignedcertificate.pem -req -signkey privateKey.pem -days 365
Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r-- 1 patrick admin 1740 16 Mai 05:23 csr.pem
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
-rw-r--r-- 1 patrick admin 1980 16 Mai 05:39 selfsignedcertificate.pem
Patricks-MBP:ssl patrick$ rm csr.pem
Patricks-MBP:ssl patrick$ ls -l
total 24
-rw-r--r-- 1 patrick admin 3247 16 Mai 05:22 privateKey.pem
-rw-r--r-- 1 patrick admin 1980 16 Mai 05:39 selfsignedcertificate.pem
show certificate details
Patricks-MBP:ssl patrick$ pwd
/usr/local/etc/nginx/ssl
Patricks-MBP:ssl patrick$ openssl x509 -in selfsignedcertificate.pem -text -noout
Configure nginx Servers with SSL
In our configuration we enforce ssl. Therefore we create a default Webserver listening on Port 80 with the server name servtest.rottlaender.lan
.
Any request to servtest.rottlaender.lan:80
is redirected to my Reverse Proxy Server which is listening on servtest.rottlaender.lan:443
.
The default Webserver is configured in /usr/local/etc/nginx/nginx.conf
.
# /usr/local/etc/nginx/nginx.conf
# default Webserver
worker_processes 1;
error_log /usr/local/etc/nginx/logs/error.log;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
access_log /usr/local/etc/nginx/logs/access.log;
# default Webserver redirect from port 80 to port 443 ssl
server {
listen 80;
listen [::]:80;
server_name servtest.rottlaender.lan;
return 301 https://$host$request_uri;
}
include servers/*;
}
The Reverse Proxy Server is configured in /usr/local/etc/nginx/servers/reverse
.
// /usr/local/etc/nginx/servers/reverse
// reverse Proxy Server
server {
listen 443 ssl;
server_name servtest.rottlaender.lan;
ssl_certificate ssl/selfsignedcertificate.pem;
ssl_certificate_key ssl/privateKey.pem;
ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:3300;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
The server name servtest.rottlaender.lan is linked in /private/etc/hosts to the ip 192.168.178.20 which is the ip of my computer in my local network.
Patricks-MBP:digitaldocblog-V3 patrick$ cat /private/etc/hosts
##
# Host Database
#
# localhost is used to configure the loopback interface
# when the system is booting. Do not change this entry.
##
127.0.0.1 localhost
255.255.255.255 broadcasthost
::1 localhost
192.168.178.20 servtest.rottlaender.lan
2. Express Secure App (Security Features HTML version)
This is a very simple application but show the basic security features you should use when you run a node app in a production environment.
The app is a website with a simple layout and navigation.
The Home page contain static information and can be accessed by everyone.
On the register page, users can find a form to register. The user data entered here are saved in the database and the user is logged in at the same time. Known users can log in with their email and password after successful registration on the login page. The login and register page can only be accessed if the user is not logged in. If a user is logged in and tries to access the login or register, he will be redirected to the dashboard page.
The dashboard is a personalized area of the website. This area can only be accessed if the user is logged in. If a user is not logged in, he will be redirected to the login page.
Logout is not really a page but a link that contains a logout function. Users who are logged in can log out using this link. Users who are not already logged in will be redirected to the login page.
Download the code from GitHub
Pls. go to my GitHub site and clone the code. Here you find a some inline documentation in the code. The details are explained in this chapter.
Create your app home directory express-security
My app home directory is different to the one that is available after you cloned the code from GitHub.
Patricks-MBP:2020-05-15-express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security
Patricks-MBP:2020-05-15-express-security patrick$ mv node-part-5-express-security-with-db-pug express-security
Patricks-MBP:2020-05-15-express-security patrick$ cd express-security
Patricks-MBP:express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security
Manage environment variables
To manage environment variables for my app I use envy
. First you need the files .env
and .env.example
in the root of your project directory. In .env.example
you create a list of all potential environment variables without any values and in .env
you use the defined variables and assign the values to them.
Patricks-MBP:express-security patrick$ ls -al
total 152
drwxr-xr-x 14 patrick staff 448 23 Jun 05:29 .
drwxr-xr-x 4 patrick staff 128 26 Mai 05:40 ..
-rw------- 1 patrick staff 181 23 Jun 05:59 .env
-rw-r--r-- 1 patrick staff 53 23 Jun 05:59 .env.example
....
Patricks-MBP:express-security patrick$ cat .env.example
port=
mongodbpath=
sessionsecret=
sessioncookiename=
Patricks-MBP:express-security patrick$ cat .env
port=<YOUR_PORT>
mongodbpath=<YOUR_CONNECTION_STRING>
sessionsecret=<YOUR_SESSION_SECRET>
sessioncookiename=<YOUR_SESSION_COOKIE_NAME>
Patricks-MBP:express-security patrick$
Envy must be installed as dependency and required in the main application file secserver.js. Then you can set the environment variables as follows.
// secserver.js
....
// envy module to manage environment variables
const envy = require('envy');
// set the environment variables
const env = envy()
const port = env.port
const mongodbpath = env.mongodbpath
const sessionsecret = env.sessionsecret
const sessioncookiename = env.sessioncookiename
....
Start the MongoDB Server
To run the db server we install mongoose
as dependency and require it in the db.js configuration file. The database connection will be initiated with mongoose.connect
and the StartMongoServer function will be exported to be called in the main application file secserver.js.
const envy = require('envy')
const env = envy()
const mongodbpath = env.mongodbpath
const mongoose = require('mongoose');
mongoose.set('useNewUrlParser', true);
mongoose.set('useUnifiedTopology', true);
const StartMongoServer = async function() {
try {
await mongoose.connect(mongodbpath)
.then(function() {
console.log(`Mongoose connection open on ${mongodbpath}`);
})
.catch(function(error) {
console.log(`Connection error message: ${error.message}`);
})
} catch(error) {
res.json( { status: "db connection error", message: error.message } );
}
};
module.exports = StartMongoServer;
Authentication and authorization
For user authentication we use the module express-session
and to store session data in the session store in our database we use connect-mongodb-session
. Therefore we install these modules as dependencies in our project and require the modules in our secserver.js main application file.
Then we create with new MongoDBStore
a session storage in our MongoDB to store session data in collection col_sessions
. errors are catched with store.on
.
We use the session in our app with app.use( session({...}) )
. With every request to our site a new session object is created with a unique session ID which include a session cookie object. The session object has keys options and the values for each key define how to deal with the session object. The session ID is created and signed using the secret
option. We use name
to provide a session cookie name and store
to define where the session object should be stored (in case we store the session).
We can access the session object with req.session
and the session ID with req.session.id
. With every request we have a new session and this new session will be created but not stored anywhere so far. We say the session is uninitialized. The saveUninitialized
false option ensure that a session will only be written to the store in case it has been modified. What does this mean?
We can modify the session when we store additional data into it. We always do this when the user is logging in via the login
or the register
route. When we post the data from the login- or from the registration-form to the server we call loginUser or the createUser module which is defined in database/controllers/userC.js
. Both modules do basically the same thing: They create a userData Object and store the userData object into the session object and redirect the user to the dashboard when login or registration was successful.
....
var userData = {
userId: user._id,
name: user.name,
lastname: user.lastname,
email: user.email,
role: user.role
}
req.session.userData = userData
res.redirect('/dashboard')
....
If the user is successfully logged in the session is initialized (modified), the session object incl. the userData object are stored into the store and a cookie is stored into the requesting browser. The content of the cookie is only a hash of the session Id and with each request of a logged in user the session on the server is looked up.
The cookie in the browser will live max 1 week as we defined in the cookie object maxAge
set to 1 week. Because of the cookie option sameSite
true the cookie scope is limited to the same site.
Then the resave
false option ensures that the session will not be updated with every request. This mean the session ID that has been created when the user has logged in will be kept until the user is logged out again.
// secserver.js
....
// server side session and cookie module
const session = require('express-session');
// mongodb session storage module
const connectMdbSession = require('connect-mongodb-session');
....
// Create MongoDB session storage
const MongoDBStore = connectMdbSession(session)
const store = new MongoDBStore({
uri: mongodbpath,
collection: 'col_sessions'
});
// catch errors in case store creation fails
store.on('error', function(error) {
console.log(`error store session in session store: ${error.message}`);
});
// Create the express app
const app = express();
....
// use session to create session and session cookie
app.use(session({
secret: sessionsecret,
name: sessioncookiename,
store: store,
resave: false,
saveUninitialized: false,
// set cookie to 1 week maxAge
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7,
sameSite: true
},
}));
....
Secure HTTP headers
Response headers are HTTP header that come with the HTTP response from the server to the client. The http response header contain data that could possibly damage the integrity of the client. It is therefore important to secure the response header of your application.
To secure the http response headers I user the module helmet. This is a relatively easy-to-use module consisting of various middleware functionalities to secure various http response headers.
First we install helmet
as a dependency of our project. Then we require helmet and use helmet right after we created the app.
// secserver.js
// hTTP module
const http = require('http');
// express module
const express = require('express');
// hTTP header security module
const helmet = require('helmet');
// Create the express app
const app = express();
....
// use secure HTTP headers using helmet
app.use(helmet())
Using simply app.use(helmet())
set the http header security to default. Then the following 7 out 11 helmet features can be used.
- dnsPrefetchControl controls browser DNS prefetching
- frameguard to prevent clickjacking
- hidePoweredBy to remove the X-Powered-By header
- hsts for HTTP Strict Transport Security
- ieNoOpen sets X-Download-Options for IE8+
- noSniff to keep clients from sniffing the MIME type
- xssFilter adds some small XSS protections
When we then request our home page to retrieve the http headers using curl -k --head
in the terminal we see the following output.
Patricks-MBP:express-security patrick$ curl -k --head https://servtest.rottlaender.lan
HTTP/1.1 200 OK
Server: nginx/1.19.0
Date: Fri, 26 Jun 2020 16:14:14 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 1734
Connection: keep-alive
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: noopen
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
ETag: W/"6c6-U2uWyDNyzlyBAbSI/Quxqo9RRQE"
Patricks-MBP:express-security patrick$
App routing
get routes: We have the following get
routes and navigation.
- Home (/)
- Login (/login)
- Register (/register)
- Dashboard (/dashboard)
- Logout (/logout)
get
routes involve an optional middleware and respond HTML back to the client.
app.get('/<route>', <optional: someMiddleware>, (req, res) => {
res.send(`<some HTML>`)
})
I will not explain the HTML and css in detail. But as everyone can see, the HTML is the same for every route except for the <body>
. Of course, this is not very nice and becomes a bit more efficient with the use of a template engine, which I will explain below using the PUG template engine. I will then rebuild the app using PUG.
Lets have a look at the middleware
. If a request is made for a route and a middleware function is included, the middleware function is first executed before the next routing function function(req, res)
is called. A condition is built into the middleware function which is checked. My middleware is built so that in case the condition is true the middleware code is executed directly and the next routing function is omitted. If the condition is false, the next routing function function(req, res)
is called.
I have built 2 different middleware functions which each check
middleware 1 (login- and register route): a user is logged in
// secserver.js
....
// middleware 1 to redirect authenticated users to their dashboard
const redirectDashboard = (req, res, next) => {
if (req.session.userData) {
res.redirect('/dashboard')
} else {
next()
}
}
....
If a user is logged in the request should be redirected to the dashboard route, in any other case (user is not logged in) the next routing function function(req, res)
is called and respond the HTML to the browser. This middleware 1 is included in the /login- and /register route. This mean logged in users will be redirected to their dashboard, not logged in users will see the login- and register form.
middleware 2 (dashboard- and logout route): a user is not logged in.
// secserver.js
....
// middleware 2 to redirect not authenticated users to login
const redirectLogin = (req, res, next) => {
if (!req.session.userData) {
res.redirect('/login')
} else {
next()
}
}
....
If a user is not logged in the request should be redirected to the login roure, in any other case (user is logged in) the next routing function function(req, res)
is called and respond the HTML to the browser. This middleware 2 is included in the /dashboard- and /logout route. This mean not logged in users will be redirected to login route, logged in users will see the dashboard- or can log themselves out.
post routes: We have the following post
routes.
- /login
- /register
The login and the register get
routes contain a form in the HTML. With these forms the user provide the data to login and for user registration. When the user click the send button the action is to call the login- or register post
route. This will happen for all not logged in users. The login and the register get
routes have the middleware redirectDashboard
to redirect the user to the dashbard if the user is already logged in.
// secserver.js
....
app.get('/login', redirectDashboard, (req, res) => {
....
res.send(`
....
<div class="form">
<form id='register_form' method='post' action='/register'>
......
<label for='send'>
<input class='sendbutton' type='submit' name='send' value='Send'>
</label>
</form>
</div>
`)
)}
....
app.get('/register', redirectDashboard, (req, res) => {
....
res.send(`
....
<div class="form">
<form id='login_form' method='post' action='/login'>
......
<label for='send'>
<input class='sendbutton' type='submit' name='send' value='Send'>
</label>
</form>
</div>
`)
)}
.....
The post
routes contain functions to login- (loginUser) or register (createUser) the user.
// secserver.js
....
// Post routes to manage user login and user registration
app.post('/login', userController.loginUser);
app.post('/register', userController.createUser);
....
The loginUser
function is defined in the user controller database/controllers/userC.js
. This function lookup a user in the database based on the email address that has been provided by the request body. The data that are attached to the request body have been provided by the user vie the login form of the app. If no user could be found in the database login is not possible. If a user exist with the given email address then the provided password will be compared with the one stored in the database. If the password match fail login is not possible because the provided password is wrong. in any other case, the login takes place and a userData object is created and attached to the session object.
// database/controllers/userC.js
User.findOne({ email: req.body.email }, function(error, user) {
if (!user) {
res.status(400).send({ code: 400, status: 'Bad Request', message: 'No User found with this email' })
} else {
if (bcrypt.compareSync(req.body.password, user.password)) {
var userData = { userId: user._id, name: user.name, lastname: user.lastname, email: user.email, role: user.role }
req.session.userData = userData
res.redirect('/dashboard')
} else {
res.status(400).send({ code: 400, status: 'Bad Request', message: 'Wrong User password' })
}
}
})
}
The createUser
function is also defined in the user controller database/controllers/userC.js
. This function create a new User object based on the data from the request body provided by the user via the form. The provided password will be hashed and stored together with all other data into the database. Finally a userData object is created and attached to the session and the user will be redirected to the dashboard after the registration was successful.
// database/controllers/userC.js
createUser: async function (req, res) {
// assign input data from request body to input variables
const name = req.body.name
const lastname = req.body.lastname
const email = req.body.email
const password = req.body.password
const role = req.body.role
const newUser = new User({
name: name,
lastname: lastname,
email: email,
password: password,
role: role
})
newUser.password = await bcrypt.hash(newUser.password, saltRounds)
await newUser.save(function(err, user) {
if (err) {
// if a validation err occur end request and send response
res.status(400).send({ code: 400, status: 'Bad Request', message: err.message })
} else {
// req.session.userId = user._id
var userData = { userId: user._id, name: user.name, lastname: user.lastname, email: user.email, role: user.role }
req.session.userData = userData
res.redirect('/dashboard')
}
})
},
And we have a default get
route.
- /favicon.ico
Browsers will by default try to request /favicon.ico from the root of a hostname, in order to show an icon in the browser tab. As we dont use favicon so far we must avoid that these requests returning a 404 (Not Found). Here The /favicon.ico request will be catched and send a 204 No Content status.
// secserver.js
....
app.get('/favicon.ico', function(req, res) {
console.log(req.url);
res.status(204).json({status: 'no favicon'});
});
....
3. Express App (Pug Template Version)
From a functional point of view this app is pretty much the same app then the HTML version. The difference is that we use PUG templates instead of HTML in each res.send().
Setup a seperate Database
For the PUG version of my app I set up a new database to manage the users and the sessions.
#> mongo
MongoDB shell version v4.2.3
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
Implicit session: session { "id" : UUID("b3e7f48a-a05c-4894-87db-996cb34eb1fb") }
MongoDB server version: 4.2.3
> db
test
> use admin
switched to db admin
> db
admin
> db.auth("adminUser", "adminpassword")
1
> show dbs
admin 0.000GB
config 0.000GB
express-security 0.000GB
local 0.000GB
> use express-security-pug
switched to db express-security-pug
> db.createUser({ user: "owner_express-security-pug", pwd: "passowrd", roles: [{ role: "dbOwner", db: "express-security-pug" }] })
Successfully added user: {
"user" : "owner_express-security-pug",
"roles" : [
{
"role" : "dbOwner",
"db" : "express-security-pug"
}
]
}
> db
express-security-pug
> exit
Connection string to connect to express-security-pug db using the owner_express-security-pug user.
mongodb://owner_express-security-pug:password@localhost/express-security-pug
Download the code from GitHub
Pls. go to my GitHub site and clone the code. Here you find some inline documentation in the code.
Create your app home directory express-security-pug
My app home directory is different to the one that is available after you cloned the code from GitHub.
Patricks-MBP:2020-05-15-express-security patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security
Patricks-MBP:2020-05-15-express-security patrick$ mv node-part-5-express-security-with-db-pug express-security-pug
Patricks-MBP:2020-05-15-express-security patrick$ cd express-security-pug
Patricks-MBP:express-security-pug patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security-pug
Install PUG and use it in your app
First we install PUG as a dependency.
Patricks-MBP:express-security-pug patrick$ pwd
/Users/patrick/software/dev/node/articles/2020-05-15-express-security/express-security-pug
Patricks-MBP:express-security-pug patrick$ npm install pug --save
PUG is already fully integrated into Express. Pls. read the documentation how to use template engines in Express.
After you installed PUG the view engine must be set in your main application file secserverpug.js.
// secserverpug.js
....
// use Pug Template Engine
app.set('view engine', 'pug')
app.set('views', './views')
....
These instructions tell your app that PUG template engine is used and that the templates can be found in /views
directory.
PUG Directory setup
In /views
I setup the templates for home, login, registration and an error template.
In /views/includes
I setup the files containing HTML or JavaScript. These can be included in the templates.
Patricks-MBP:express-security-pug patrick$ ls -l
total 128
-rw-r--r-- 1 patrick staff 771 1 Jul 06:04 README.md
drwxr-xr-x 5 patrick staff 160 29 Jun 05:26 database
drwxr-xr-x 150 patrick staff 4800 29 Jun 06:11 node_modules
-rw-r--r-- 1 patrick staff 47547 29 Jun 06:11 package-lock.json
-rw-r--r-- 1 patrick staff 367 29 Jun 06:11 package.json
-rw-r--r-- 1 patrick staff 4393 2 Jul 05:34 secserverpug.js
drwxr-xr-x 3 patrick staff 96 29 Jun 05:26 static
drwxr-xr-x 8 patrick staff 256 2 Jul 05:45 views
Patricks-MBP:express-security-pug patrick$ ls -l views
total 40
-rw-r--r-- 1 patrick staff 549 30 Jun 05:35 dashboard.pug
-rw-r--r-- 1 patrick staff 522 2 Jul 05:50 err.pug
-rw-r--r-- 1 patrick staff 420 29 Jun 05:39 home.pug
drwxr-xr-x 6 patrick staff 192 29 Jun 05:17 includes
-rw-r--r-- 1 patrick staff 735 30 Jun 05:02 login.pug
-rw-r--r-- 1 patrick staff 1067 30 Jun 05:08 register.pug
Patricks-MBP:express-security-pug patrick$ ls -l views/includes
total 32
-rw-r--r-- 1 patrick staff 76 29 Jun 05:39 foot.pug
-rw-r--r-- 1 patrick staff 167 29 Jun 05:24 head.pug
-rw-r--r-- 1 patrick staff 489 2 Jul 05:13 nav.pug
-rw-r--r-- 1 patrick staff 420 29 Jun 05:08 script.js
Patricks-MBP:express-security-pug patrick$
The responsive Website Design
Each site like home, login, register and dashboard has a specific site template in /views
directory. The site content will be defined in the main section of each template. PUG enables files with HTML or JavaScript to be included. This makes the site templates clear and easy to maintain. The includes are located in /views/includes
directory.
The website is build based on a grid design and each site template has the following structure.
doctype html
HTML
Head
include includes/head.pug
Body
Grid-Container
Header
include includes/nav.pug
Main
... site template specific HTML ...
Footer
include includes/foot.pug
<script>
include includes/script.js
The design of the website is defined in the css in static/css/style.css
.
Here in the css we define the Site Structure as grid areas consisting of header, main and footer and link them to the grid-container.
....
.header { grid-area: header; background-color: #ffffff; border-radius: 5px;}
.main { grid-area: main; background-color: #ffffff; border-radius: 5px;}
.footer { grid-area: footer; background-color: #ffffff; border-radius: 5px;}
.grid-container {
display: grid;
grid-template-areas:
"header"
"main"
"footer";
grid-gap: 5px;
background-color: #d1d1e0;
padding: 50px;
}
....
The Navigation is defined in the Header area of the Grid-Container and the HTML comes into the template via include includes/nav.pug
.
// includes/nav.pug
//(this) refers to the DOM element to which the onclick attribute belongs to
// the a DOM element will be given as parameter to the function
a(class="burgericon" onclick="myFunction(this)")
div(class='burgerline' id='bar1')
div(class='burgerline' id='bar2')
div(class='burgerline' id='bar3')
a(class='link' href='/') Home
a(class='link' href='/login') Login
a(class='link' href='/register') Register
a(class='link' href='/dashboard') Dashboard
a(class='link' href='/logout') Logout
So the navigation design is then defined in the css. Each navigation object is an a
link. We have a
links with class link
and burgericon
. The burgericon is used to open the navigation bar onclick when the screen is smaller than 600px (like iphone displays etc., explained below), is cosisting of 3 burgerlines and these lines are created using 3 div objects with class burgerline
. The burgelines will be transformed with speed 0.4s when you click on the burgericon (explained below). The burgericon is not visible and aligned on the right edge. All other navigation links are visible and aligned on the left edge.
/* static/css/style.css */
....
/* style the navigation links with float on the left (side by side) */
.header a.link {
float: left;
display: block;
padding: 14px 16px;
text-decoration: none;
font-size: 1.4vw;
color: #28283e;
}
/* hover effect for each navigation link */
.header a.link:hover {
background-color: #28283e;
color: #ffffff;
}
/* style the burgericon link on the right */
.header a.burgericon {
float: right;
display: none;
padding: 14px 16px;
}
/* style each burgerline that create the burgericon */
.burgerline {
width: 35px;
height: 5px;
background-color: #28283e;
margin: 6px 0;
transition: 0.4s;
}
....
When the display screen is lower than 600px the navigation links will not be shown and the burgericon (on the right side) will be faded in instead.
/* static/css/style.css */
....
/* for screens up to 600px remove the navigation links and show the burgericon instead */
@media screen and (max-width: 600px) {
.header a.link { display: none; }
.header a.burgericon { display: block; }
}
....
When you click on the burgericon the burgerlines will be transformed so that you will see a cross instead of the hamburger like icon. The 2nd burgerline with id='bar2'
will not be shown at all while the other 2 burgelines will be rotated 45 degrees counterclockwise (burgerline with id='bar1'
) and clockwise (burgerline with id='bar1'
).
/* static/css/style.css */
....
/* style burgerlines after on onclick event */
/* the .change class will be added onclick with classList.toggle in the JavaScript */
/* rotate first bar */
.change #bar1 {
/* rotate -45 degrees (counterclockwise) move 15px down in Y-direction */
transform: rotate(-45deg) translateY(15px);
}
/* fade out the second bar */
.change #bar2 {
opacity: 0;
}
/* rotate third bar */
.change #bar3 {
/* rotate +45 degrees (clockwise) move 15px up in Y-direction */
transform: rotate(45deg) translateY(-15px);
}
....
After clicking on the burgericon, the burgerlines are transformed as described. The links of the navigation menu are displayed one below the other (float none) and aligned left.
/* static/css/style.css */
....
/* for screens up to 600px and after onclick event the responsive class will be added to the header */
@media screen and (max-width: 600px) {
/* show navigation links left with no float (links shown among themselves) */
.header.responsive a.link {
float: none;
display: block;
text-align: left;
}
}
....
All onclick functionalities are controlled by the javascript which is embedded in the HTML of each site template (pls see above includes/nav.pug). In the HTML, the onclick event is initiated in the burgericon link and the function myFunction is called with onclick =" myFunction(this) "
. With the parameter this the entire burgericon object is transferred to the javascript function.
With each click on the burger icon, the class change
is added to each burgerline or, if available, removed. This is done by the toggle() function. If change
is set, the hamburg icon is transformed into a cross according to the specification in the css (see above). If change
is withdrawn with a new click, the hamburger icon is displayed again.
But it happens even more in the javascript when you click on the hamburger icon. The element that has the id responsivenav
is searched for and the variablereponsiveNavElement
is assigned to this element. Is the class of the reponsiveNavElement header
the classresponsive
is added after clicking on the hamburger icon. If the class responsive
is set, as described above, the links of the navigation menu are displayed one below the other (float none) and aligned left. So it applies in the css .header.responsive a.link {....}
In all other cases only the class header
is set. So it applies in the css .header a.link {....}
and the navigation links are not shown.
// includes/script.js
// the (burgerlines) parameter represent the DOM element that has been given to the function
function myFunction(burgerlines) {
burgerlines.classList.toggle('change');
var reponsiveNavElement = document.getElementById('responsivenav');
if (reponsiveNavElement.className === 'header') {
reponsiveNavElement.classList.add('responsive')
} else {
reponsiveNavElement.className = 'header';
}
}
Finally at the end of the css we define the defaults for h1, for our text content, the forms, the input fields and the send buttons.
Summary and Outlook
In this part 4 of my little node.js series we have seen how to setup a production ready environment for our express app. I showed this using a Mac OS, but in principle this setup also applies to Linux, for example.
The basic setup is, to put it simply, the app runs as a service on the server in the background using a Process Manager, but has no interface to the client. This client interface regulates a reverse proxy which is upstream of the app and accepts all requests and forwards them to the app, as well as the responses from the app back to the client. The communication is SSL/TLS secured.
At the center of the setup is a separate local MongoDB that manages all application data. In our example, these are the users, but also the sessions. I prefer to set up my own MongoDB on my server but of course it is conceivable to use a cloud-based solution or to install another database locally.
The express app itself uses secure HTTP response headers so that HTTP attacks like clickjacking, MIME type sniffing or some smaller XSS attacks on the client are made as difficult as possible. Access to personal areas of the application is secured by session-based authentication and authorization. The session-relevant data is stored in the database and not in the browser cookie, which means additional security with regard to attacks on the client. The browser cookie only contains a hash of the session ID to query the relevant user data from the database.
I would like to end my node.js series with Part 4. I discussed and demonstrated the basic concepts and procedures in parts 1 to 4. Of course there will be other interesting articles on the topic node.js and web programming on Digitaldocblog. Just take a look.