Inevitably, most web software is meant to be on the open web. We want to enable our users to work anywhere in the universe they might be. But, we don't want our list of employees to be open to the world wide web. For this, we must add a log in.
This system is pretty extensive. We need a way for people to log in, a way to check that someone has actually logged in before we allow access to resources, a way to bootstrap the first user (aka create the first user), and an admin system that allows us to add and delete users and reset passwords.
Not a small task, for sure. Building a log in system has been broken up into four parts for this reason.
The first thing we need to do is create a new table for our users.
create table users (
id int unsigned not null,
obfuscatedId char(16) not null,
created datetime not null,
updated timestamp not null default current_timestamp on update current_timestamp,
lastLogin datetime null,
email varchar(252) not null,
username varchar(128) not null,
passwordHash char(255) null,
forcePasswordChange boolean not null default true,
isEnabled boolean not null default true,
isDeleted boolean not null default false
) engine=innodb default charset=latin1;
alter table users
add primary key (id);
alter table users
modify id int unsigned not null auto_increment;
We're using some of the concepts that we've learned before. For example, for each user, we'll have an obfuscatedId. We're also using an isEnabled and a isDeleted field.
You'll see other fields that are useful to store, such as email, username, passwordHash, and forcePasswordChange.
email is useful to store for workflow purposes. In a lot of applications, you might have many different people interacting with different parts of the application. You can think of user A works on section A, user B works on section B, and so on. It's sometimes nice to get notifications when someone is done doing something. For example, if user A finishes a task, you can then send an email to user B to start their task. You might not need to store email though, and that's okay.
In our application, we're going to force users to log in with a username, instead of an email. Forcing usernames makes the application log in more secure. Hackers can easily find an email address and therefore they have half the log in. Usernames are usually pretty unique and hard to guess. Some applications don't need this though, and it's fine to match email addresses and passwords. It's pretty trivial to change to using email instead of usernames.
Let's talk about password storage. You'll see that instead of naming our password field password, we've named it passwordHash. Why would we do that? It's a reminder that our passwords are not stored in plain text. Our passwords are stored in an encrypted format.
Our encrypted format is specifically called an Encrypted Hash or Hash for short. What is a hash? It's an encryption function that takes a piece of text and turns it into a piece of encrypted text. Specifically, this hash function is a one way encryption function. What does this mean? Hash functions are designed to only encrypt. There is no decrypt function.
Let's take a look at a hash function that is built into PHP. It's called password_hash() and is specifically designed for password storage.
echo password_hash("asdf1234", PASSWORD_DEFAULT);
might echo something like:
$2y$10$m/XAxpn2AOJtt9Ga5BbFmeHg8aT48bzMIk/NxfeWyCfFsoRNdmTKW
If you run the password_hash function multiple times, you'll see that the output changes every time, even though the password you provided is the same. This is a good thing. For more information about why this is a good thing, read the Wikipedia Article about this subject.
We'll go more into how we match a password to a passwordHash in the section about log ins, which is covered later.
In our second step, we will build a quick tool to create our initial user. This is a bootstrap tool so that we have our first user. Once you are done running it the first time, it should be deleted. You don't want this accessible from the web by anyone random.
Our new tool is going to be quick and dirty and only needs to contain the basics to get things going in a fresh database.
Put this into your init-user.php file.
<?php
require_once "db.php";
use \database\db;
// set user
$email = "[enter your email address]";
$username = "root";
$password = "[enter a 10 or more character password here]";
// create obfuscatedId
$obfuscatedId = bin2hex( random_bytes(8) );
// create hashed password
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
// make sure the password was actually hashed
if ($passwordHash != false) {
$db = new db;
$db->query(
"insert into users set created = now(), lastLogin = now(), obfuscatedId = :obfuscatedId, email = :email, username = :username, passwordHash = :passwordHash, forcePasswordChange = false",
[ ":obfuscatedId"=>$obfuscatedId, ":email"=>$email, ":username"=>$username, ":passwordHash"=>$passwordHash, ]
);
echo "done<br>";
} else {
echo "there was an error encrypting the password.<br>";
}
Go ahead and run the tool from your browser.
The tool should output done.
From there, we'll double check our MySQL table to see if everything is filled in correctly:
mysql> select id, obfuscatedId, lastLogin, username, passwordHash from users;
+----+------------------+---------------------+----------+--------------------------------------------------------------+
| id | obfuscatedId | lastLogin | username | passwordHash |
+----+------------------+---------------------+----------+--------------------------------------------------------------+
| 1 | e6d05d0e8d145c4f | 2020-04-06 21:53:04 | root | $2y$10$ymLUBfsfPQBpa5540Viuae9CyPcPJ75lAcOV9AHDmDJKZEHJhdwzC |
+----+------------------+---------------------+----------+--------------------------------------------------------------+
1 row in set (0.00 sec)
Everything looks good!
We only need to run this tool one time. If you feel like things don't look right, just mysql> truncate users; and run it again.
WARNING: Delete init-user.php after you have used it...