Home  /  Documentation  /  Tutorials  /  Putting your web application online – Part 5 – Database and USERS

Putting your web application online – Part 5 – Database and USERS

Previous part: Putting your web application online – Part 4 – HTTPS


Starting point:You have a Node++ application on the server with HTTPS enabled.
This part result:You can create a user account, log in, change profile details, reset password, send a message, etc.
Skill level:Middle
Time:About 60 minutes

Steps to take:

  1. Install database server
  2. Create users database
  3. Node++ database setup
  4. Node++ USERS setup
  5. CSRF protection

Step 1 – Install database server

Looking at modern database servers landscape, we have basically two types: SQL (structured) and NoSQL (sometimes nicknamed as document). NoSQL databases are primarily optimized for large unstructured data volumes, like for example social network user content. However in majority of everyday use cases, traditional SQL model is still preferred. Modern engines of both types are anyway capable of serving variety of needs.

Node++ USERS module uses SQL model and its most popular engine: MySQL.

Among Linux packages we can encounter two database servers with the same API: MySQL and MariaDB. While the latter is newer and – is some circumstances – faster, from a moderate-sized startup standpoint, it's practically the same.

On AWS EC2 Linux 2 AMI, default MySQL-compatible package is MariaDB:

$ sudo yum install mariadb-server $ sudo yum install mariadb-devel

For other distributions see Cheat Sheets.

We should start with securing our database server. Make sure the root password is secure. You can use our secure password generator for this:

$ sudo mysql_secure_installation

As default timeout is quite short, we need to increase it. Also default packet size is typically very modest. If we want for example, save pictures to the database, we need to increase it as well. Let's edit the configuration:

$ sudo vi /etc/my.cnf

Our changes should go into the [mysqld] section:

wait_timeout=31536000 # (to prevent disconnecting after inactivity) max_allowed_packet=16M # (to allow bigger packets between app and server)

Lets' start the server and add it to system startup:

$ sudo systemctl start mariadb $ sudo systemctl enable mariadb

To improve our data security we can create a separate user for our application. Let's start mysql client:

$ mysql -u root --password=<root password>

First, lets' create a database:

mysql> CREATE DATABASE nodepp;

Then create user nodepp:

mysql> CREATE USER 'nodepp'@'localhost' IDENTIFIED BY '<nodepp password>';

Now grant the privileges only to the database:

mysql> GRANT ALL PRIVILEGES ON nodepp.* TO 'nodepp'@'localhost';

Refresh:

mysql> FLUSH PRIVILEGES;

Quit the mysql client:

mysql> quit

Step 2 – Create users database

It only requires execution of Node++ SQL script:

$ mysql -u nodepp --password=<nodepp password> nodepp < ~/dev/lib/users.sql

Step 3 – Node++ database setup

Node++ compilation script can link MySQL libraries and application can open a database connection at startup. It requires adding NPP_MYSQL to npp_app.h:

#define NPP_MYSQL

We would obviously have to recompile the application but let's wait with this until the next step.

Additionally, we need to tell the compiler where the MySQL headers and libraries are installed. We use src/m script for this. On AWS EC2 Linux 2 AMI they are there:

# Include path: export CPATH=/usr/include/mysql # Library path: export LIBRARY_PATH=/usr/lib64/mysql

For other distributions see Cheat Sheets.

We also need to tell our application where the database is and how to get there. Let's edit the configuration, that is bin/npp.conf:

dbName=nodepp dbUser=nodepp dbPassword=<nodepp password>

Step 4 – Node++ USERS setup

To enable USERS module we need to add NPP_USERS to npp_app.h:

#define NPP_USERS

Before we continue with coding, we need to make two choices:

  1. How do we want to identify our users?

    We can choose between login and email. As login is default, for email we'd need to define NPP_USERS_BY_EMAIL in npp_app.h. In such a case login won't be required in HTML forms. Note that with default option email can also be used to log in.

  2. Do we want users to confirm registration via email?

    This can actually be decided at the configuration stage. By default, confirmation is not required. If we want this, usersRequireActivation parameter in npp.conf needs to be > 0.

Now is the largest part of our task: we need to write HTML forms. Let's call each of the functions render_action, where action can be one of the following:

We don't have to implement all of them at once. The minimum set to get started would require render_register() and render_login().

render_register() example:

void render_register() { render_header(); OUT("<h2>Register</h2>"); OUT("<style>"); OUT(".label{display:inline-block;width:200px;text-align:right;}"); OUT("</style>"); check_error_msg(); OUT("<form action=\"/do_register\" method=\"POST\" novalidate>"); if ( REQ_DSK ) { OUT("<p><span class=label>Login:</span> <input name=\"login\" autofocus></p>"); OUT("<p><span class=label>Email:</span> <input type=\"email\" name=\"email\"></p>"); OUT("<p><span class=label>Name:</span> <input name=\"name\"></p>"); OUT("<p><span class=label>Password:</span> <input type=\"password\" name=\"passwd\"></p>"); OUT("<p><span class=label>Repeat password:</span> <input type=\"password\" name=\"rpasswd\"></p>"); OUT("<p><span class=label></span><input type=\"submit\" value=\"Register\"></p>"); } else /* phone */ { OUT("<p>Login:</p>"); OUT("<p><input name=\"login\" autofocus></p>"); OUT("<p>Email:</p>"); OUT("<p><input type=\"email\" name=\"email\"></p>"); OUT("<p>Name:</p>"); OUT("<p><input name=\"name\"></p>"); OUT("<p>Password:</p>"); OUT("<p><input type=\"password\" name=\"passwd\"></p>"); OUT("<p>Repeat password:</p>"); OUT("<p><input type=\"password\" name=\"rpasswd\"></p>"); OUT("<p><input type=\"submit\" value=\"Register\"></p>"); } OUT("</form>"); RES_DONT_CACHE; render_footer(); }

render_login() example:

void render_login() { render_header(); OUT("<h2>Login</h2>"); OUT("<style>"); OUT(".label{display:inline-block;width:200px;text-align:right;}"); OUT("</style>"); check_error_msg(); OUT("<form action=\"/do_login\" method=\"POST\" novalidate>"); if ( REQ_DSK ) { OUT("<p><span class=label>Login or email:</span> <input name=\"login\" autofocus></p>"); OUT("<p><span class=label>Password:</span> <input type=\"password\" name=\"passwd\"></p>"); OUT("<p><span class=label></span><label><input type=\"checkbox\" name=\"keep\" checked> Remember me</label></p>"); OUT("<p><span class=label></span><input type=\"submit\" value=\"Login\"></p>"); } else /* phone */ { OUT("<p>Login or email:</p>"); OUT("<p><input name=\"login\" autofocus></p>"); OUT("<p>Password:</p>"); OUT("<p><input type=\"password\" name=\"passwd\"></p>"); OUT("<p><label><input type=\"checkbox\" name=\"keep\" checked> Remember me</label></p>"); OUT("<p><input type=\"submit\" value=\"Login\"></p>"); } OUT("</form>"); OUT("<div>"); OUT("<p>or <a href=\"/register\">Register</a></p>"); OUT("</div>"); OUT("<div>"); OUT("<p><a href=\"/forgot\">I forgot my password</a></p>"); OUT("</div>"); RES_DONT_CACHE; render_footer(); }

Note check_error_msg() at the beginning. If there's an msg parameter in the query string, it will show the message above the form:

void check_error_msg() { int msg; if ( !QSI("msg", &msg) || msg==OK ) return; OUT("<style>"); OUT(".msg_red{width:100%;text-align:center;color:red;}"); OUT(".msg_orange{width:100%;text-align:center;color:orange;}"); OUT(".msg_green{width:100%;text-align:center;color:green;}"); OUT("</style>"); if ( MSG_CAT_RED(msg) ) /* error */ OUT("<p class=\"msg_red\">%s</p>", npp_message(msg)); else if ( MSG_CAT_ORANGE(msg) ) /* warning */ OUT("<p class=\"msg_orange\">%s</p>", npp_message(msg)); else /* success */ OUT("<p class=\"msg_green\">%s</p>", npp_message(msg)); }

Form fields have to have specific names, listed in USERS functions' documentation.

We also need to update routing in npp_app_main(). Some of this routing details will depend on the application model. From the backend routing point of view, it can be traditional, SPA or mixed combination of those two. The example below is for the traditional model, in which AJAX is not used and every change on the page requires its reloading.

Functions render_* render HTML forms with attribute action pointing to the relevant do_* endpoint:

void npp_app_main() { int ret=OK; if ( REQ("login") ) { if ( LOGGED ) RES_LOCATION("/dashboard"); else render_login(); } else if ( REQ("do_login") ) { ret = npp_usr_login(); if ( ret == OK ) RES_LOCATION("/dashboard"); else RES_LOCATION("/login?msg=%d", ret); } else if ( REQ("register") ) { render_register(); } else if ( REQ("do_register") ) { ret = npp_usr_create_account(); if ( ret == OK ) RES_LOCATION("/login?msg=%d", MSG_WELCOME_NO_ACTIVATION); /* or MSG_WELCOME_NEED_ACTIVATION */ else RES_LOCATION("/register?msg=%d", ret); } else if ( REQ("contact") ) { render_contact(); } else if ( REQ("do_contact") ) { ret = npp_usr_send_message(); if ( ret == OK ) RES_LOCATION("/contact?msg=%d", MSG_MESSAGE_SENT); else RES_LOCATION("/contact?msg=%d", ret); } else if ( REQ("myaccount") ) { render_myaccount(); } else if ( REQ("do_save_myaccount") ) { ret = npp_usr_save_account(); if ( ret == OK ) RES_LOCATION("/myaccount?msg=%d", MSG_CHANGES_SAVED); else if ( ret == MSG_ACCOUNT_DELETED ) RES_LOCATION("/farewell"); else RES_LOCATION("/myaccount?msg=%d", ret); } else if ( REQ("logout") ) { npp_usr_logout(); RES_LOCATION("/login?msg=%d", MSG_USER_LOGGED_OUT); } else if ( REQ("forgot") ) { render_forgot(); } else if ( REQ("do_forgot") ) { ret = npp_usr_send_passwd_reset_email(); if ( ret == OK ) RES_LOCATION("/forgot?msg=%d", MSG_REQUEST_SENT); else RES_LOCATION("/forgot?msg=%d", ret); } else if ( REQ("preset") ) { render_preset(); } else if ( REQ("do_preset") ) { ret = npp_usr_reset_password(); if ( ret == OK ) RES_LOCATION("/login?msg=%d", MSG_PASSWORD_CHANGED); else RES_LOCATION("/preset?msg=%d", ret); } else if ( REQ("farewell") ) { render_msg(MSG_ACCOUNT_DELETED); } else if ( REQ("dashboard") ) { if ( LOGGED ) render_dashboard(); else RES_LOCATION("/login"); } else if ( /* ... */ ) ... }

Brute-force protection

Node++ USERS module has a brute-force attack protection. It records unsuccessful login attempts (ula_*) in users table, along with their time. After several ula-s, it forces the client to wait before the next attempt. The details and setup is described here.


Step 5 – CSRF protection

CSRF stands for Cross-Site Request Forgery, also known as session riding, one of the attack types, involving performing unwanted action during legitimate user session. If you are interested in the details and real attack examples, this Wikipedia article explains it quite well.

One of the common ways of web application protection against such an attack is CSRF token, in short: CSRFT. If we add freshly generated random token to every HTML form (usually as a hidden input), we can then verify it upon the form submission. This way an attacker has no way of preparing a valid request in advance.

Node++ generates fresh CSRF token every time the session is started. There are three macros, of which two are needed for a basic protection:

Our modified code would then have additional CSRFT input:

void render_login() { // ... OUT("<form action=\"/do_login\" method=\"POST\" novalidate>"); OUT_CSRFT; if ( REQ_DSK ) { OUT("<p><span class=label>Login or email:</span> <input name=\"login\" autofocus></p>"); OUT("<p><span class=label>Password:</span> <input type=\"password\" name=\"passwd\"></p>"); OUT("<p><span class=label></span><label><input type=\"checkbox\" name=\"keep\" checked> Remember me</label></p>"); OUT("<p><span class=label></span><input type=\"submit\" value=\"Login\"></p>"); } else /* phone */ { OUT("<p>Login or email:</p>"); OUT("<p><input name=\"login\" autofocus></p>"); OUT("<p>Password:</p>"); OUT("<p><input type=\"password\" name=\"passwd\"></p>"); OUT("<p><label><input type=\"checkbox\" name=\"keep\" checked> Remember me</label></p>"); OUT("<p><input type=\"submit\" value=\"Login\"></p>"); } OUT("</form>"); // ... }

On the form submission (when browser goes to do_login) we would need to check token validity before proceeding:

void npp_app_main() { // ... else if ( REQ("do_login") ) { if ( CSRFT_OK ) ret = npp_usr_login(); else ret = ERR_CSRFT; if ( ret == OK ) RES_LOCATION("/dashboard"); else RES_LOCATION("/login?msg=%d", ret); } // ... }

CSRF protection would need to be added to every form, especially vulnerable is myaccount.


That's it for now! We can now create and manage user accounts in our application.


Next part: Putting your web application online – Part 6 – Advanced anti-bot protection


Is something wrong here? Please, let us know! Envelope