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:
- Install database server
- Create users database
- Node++ database setup
- Node++ USERS setup
- 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:
-
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.
-
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:
register
(create account)
login
(log in)
logout
(log out)
myaccount
(edit account details)
forgot
(forgot password)
preset
(password reset)
contact
(send a message)
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:
OUT_CSRFT
– add hidden input with session's CSRFT
CSRFT_OK
– verify CSRFT
CSFRT_REFRESH
– refresh CSRFT (if higher security is required)
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!