Many websites use a system of user verification that requires everyone who signs up to provide an email address, and then click a link sent to them to 'activate' their account.  Though simple, this is an effective way to limit bogus account creation, and provide some security for your user-based web service.  In this tutorial we'll cover how to set up such a registration process using PHP and MySQL.  It's simple!  We'll write just a little bit of HTML to create forms, and then use PHP to code for the logic of what we want to happen.  A basic understanding of PHP, HTML and MySQL is all that's needed to follow along.

Overview of Steps:

  1. Create a User table in our database
  2. Create a form that allows for Signing In
  3. Create a form that allows for Signing Up
  4. Create an activation script

Overview of the Logic:

  1. User signs up for service, entering a valid password and email address
  2. A user entry is created in the database with a unique 'activation' string (typically some random data)
  3. The service emails the user with a link to click to 'activate' their account
  4. The link contains the 'activation' string, and points to an activation script provided by the service
  5. The activation script page verifies that the activation string sent to the user is in fact the one associated with their account, and if it is, it clears that field in the user entry
  6. The successfully activated user, i.e. the user with a blank activation field, can then login to the service!

Our final folder structure will look like this, with two actions and two views:

root
├── actions
│   ├── activate.php
│   └── pdo.php
├── login.php
└── signup.php

1. Create a table in MySQL to store your user information

We'll want it to have the following fields:

  • id - unique identifier for each user account
  • email - the user's functional username
  • password - the user's hashed password
  • activation - a field that when empty signifies an activated account
  • dateCreated - a timestamp capturing when the account was created

This can be created in MySQL as follows:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `activation` varchar(255),
  `dateCreated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
);

You can also use a graphical interface like MySQL Workbench or phpMyAdmin.
With our table created we're ready to create a our PHP/HTML forms.

2. Login Form

The first form we'll create is the login form which consists of a username and password field. Because we'll also need login to check what a user enters in the form, we'll create a PHP file. Edit login.php to look like this:

<!DOCTYPE html>
<html lang="en">

<head>
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
    <title>Login</title>
</head>

<body>
    <h1 class="w3-center">Login</h1>
    <div class="w3-card-4" style="margin-bottom: 10px;">
      <form class="w3-container" method="post">
        <p>
            <label class="w3-text-black"><b>Username</b></label>
            <input class="w3-input w3-border w3-white" type='text' name='email'placeholder="Email">
        </p>
        <p>
            <label class="w3-text-black"><b>Password</b></label>
            <input class="w3-input w3-border w3-white" type='password' name='password' placeholder="Password">
        </p>
        <p>
            <!-- <input id='submit' type='submit' name='submit' value='Login'> -->
            <button type="submit" name='submit' class="w3-bar w3-btn w3-blue">Login</button>
        </p>
      </form>
    </div>
</body>
</html>

This takes advantage of the W3.CSS responsive web design library to make the views looks nice. Visually appealing forms are just so much nicer to work with!

Though the form is done, we still need a method to connect to our database before we can proceed to write the login logic. Because the database connection code is so likely to be used often on a website, it's a good idea to factor it. Further, because we're writing a public facing form, i.e. a form that the whole wide web might enter data into, it's a good idea to take advantage of PHP's PDO family of SQL functions to reduce our vulnerability to injection attacks. Open actions/pdo.php and enter the following:

<?php
    $host = 'your-domain'; // or perhaps 'localhost'
    $db = 'database_name';
    $user = 'database_user';
    $pass = 'database_user_password';
    $charset = 'latin1';
    
    $dsn = "mysql:host=$host;dbname=$db;charset=$charset";
    $opt = [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ];

    $pdo = new PDO($dsn, $user, $pass, $opt);
?>

Save the file. We can now create a database interaction object called $pdo in any script by including 'actions/pdo.php'.

Now we're ready to create the login logic. The pseudo code is as follows:

if the form is submitted:
    if valid email and password match a user in database:
        if user is activated:
            // Good to go!
        else:
            // Suggest user check their email or activate
    else:
        // Bad Credentials!

For our HTML form, our PHP will looking something like this:

<?php
    // Login Attempted:
    if(isset($_POST['submit'])){
    	$success = false;
    	$errors = [];

    	//Get entered values:
		$email = htmlentities($_POST['email']);
		$password = $_POST['password'];
		
		// Hash the password:
		// Generally, do as little as possible with plaintext passwords, store and transmit them in hashed form:
		$password = sha1($password);

    	//Connect to DB:
		require('actions/pdo.php');

		// Get a the user 
		$stmt = $pdo->prepare("SELECT * FROM user WHERE (email=? AND password=?)");
		$stmt->execute(array($email, $password));
		$result = $stmt->fetchAll();
		
		// User with that email/password combo exists:
		if (!empty($result)) {

			// Are they activated?
			if ($result[0]['activation'] != '') {
				// Not activated!
				$errors[] = "Account not activated.  Check your email!";
			} else {
				// Good to Go!
				// Normally you would set some session vars, and redirect
				// but for demo purposes we'll just set a success var
				// so we can report on our successful login below
				$success = true;
			}
		} else {
			$errors[] = "Bad credentials!";
		}
    }
?>

Okay! This will go at the top of login.php, and will be run when the "Login" (ie. submit) button is clicked. We use an array called "errors" to collect any errors that occur during the logic so that we can display them to the user. This pattern is very helpful in simplifying error handling. In PHP, we can append to an array by assigning to the empty bracket, eg. my_array[] = 'new value";

One more thing that should be addressed here is the use of the PDO to perform an SQL query. Because we're using a prepared statement, i.e. a statement that is prepared beforehand with wildcards that will accept our input, the queries look a little different than simpler (and less secure) PHP MySQL queries. Essentially, each '?' in the query will accept one member of whatever array we execute the statement with. Order matters!

Lastly, we need a way to report errors and success to the user. In order to do this, replace the <h1>...</h1> tag with the following:

	<div class="w3-center">
		<h1 class="w3-center">Login</h1>
		
		<?php

		        // Report Form Errors:
			if (!empty($errors)) {
				foreach ($errors as $e) {
					echo "<p class='w3-center w3-red'>$e</p>";
				}
			}

			// Report success:
			if ($success) {
				echo "<p class='w3-center w3-green'>You've successfully Logged In!</p>";
			}
		?>

	</div>

This prints each error, or if $success has been set to true, reports a successful login. It also uses simple w3css to color and center the text.

Okay!

Load the result up in your browser, and no matter what you type or enter, you should see 'Bad Credentials!'. Now we need a user to continue testing!

3. Signup

Signup will follow the same basic principles as login. We can create a nice looking, simple signup form in HTML that can nicely print both errors in red, and successful messages in green:

<!DOCTYPE html>
<html lang="en">

<head>
	<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
	<title>Login</title>
</head>

<body>
	<div class="w3-center">
		<h1 class="w3-center">Signup</h1>
		<?php

			// Report Form Errors:
			if (!empty($errors)) {
				foreach ($errors as $e) {
					echo "<p class='w3-center w3-red'>$e</p>";
				}
			}

			// Report Signup Successes:
			if (!empty($messages)) {
				foreach ($messages as $m) {
					echo "<p class='w3-center w3-green'>$m</p>";
				}
			}
		?>

	</div>

	<div class="w3-card-4">
	  <form class="w3-container" method="post">
			<p>
				<label class="w3-text-black"><b>Email:</b></label>
				<input class="w3-input w3-border w3-white" type='text' name='email' size=25 placeholder="Email">
			</p>
			
			<p>
				<label class="w3-text-black"><b>Password:</b></label>
				<input class="w3-input w3-border w3-white" type='password' name='password' placeholder="Password">
			</p>
			
			<p>
				<label class="w3-text-black"><b>Retype Password:</b></label>
				<input class="w3-input w3-border w3-white" type='password' name='password2' placeholder="Confirm Password">
			</p>

			<p>
		    	<button type="submit" name='submit' class="w3-bar w3-btn w3-blue">Signup</button>
			</p>
		
		</form>
	</div>
</body>
</html>

We'll want to verify that the email is indeed a valid looking email of the form something@site.domain and that the two entered passwords match. The pseudo code for the logic will look something like this:

If form submitted:
    if no fields are blank:
        if email is valid:
            if passwords match:
                if email does not already exist in user table:
                    // Create a user!
                else:
                    // Email already in use!
            else:
                // Passwords do not match!
        else:
            // Invalid email!
    else:
        // Must fill out all fields!

Again, utilizing the same error array pattern, our logic will look something like this:

<?php

	// Check for a valid email address w/ regex:
	function verifyEmail ($email) {
	    return (preg_match("/^([[:alnum:]]|_|\.|-)+@([[:alnum:]]|\.|-)+(\.)([a-z]{2,4})$/", $email));
	}
	
	// Handle for submission:
	if(isset($_POST['submit'])) {

		// Gather data:
		$email = $_POST['email'];
		$password = $_POST['password'];
		$password2 = $_POST['password2'];


		// echo "email: " . $email . "<br>";
		// echo "password: " . $password . "<br>";
		// echo "password2: " . $password2 . "<br>";

		/* Perform Validation */
		$errors = array();
		$messages = array();

		// validate email:
		if (verifyEmail($email) != 1) {
			$errors[] = "Not a valid email address!";
		}

		// Ensure no fields are empty:
		if (empty($email) || empty($password) || empty($password2)) {
			$errors[] = "You must fill out each field!";
		}

		// Compare passwords:
		if ($password != $password2) {
			$errors[] = "Passwords do not match!";
		}

		// No errors, check for already used email:
		if (empty($errors)) {

			//Connect to DB:
			require('actions/pdo.php');

			// Make sure the email is unique:
			$stmt = $pdo->prepare("SELECT * FROM user WHERE (email=?)");
			$stmt->execute(array($email));
			$result = $stmt->fetchAll();

			if (!empty($result)) {
				// Email is already in use.
				$errors[] = "An account under that email already exists... Have you activated?";
			}
		}

		// If there have been no errors, we can proceed to create a new user entry:
		if (empty($errors)) {

			// Hash the Password:
			$password_hashed = sha1($password);

			// Activation Status String via Rand:
			$activation = md5(uniqid(rand(), true));


			// Insert the new user:
			$stmt = $pdo->prepare("INSERT INTO user (email, password, activation) VALUES (?, ?, ?)");
			$stmt->execute(array($email, $password, $activation));

			$messages[] = "You've successfully signed up!";


		}

	}

?>

A couple of things to note: 

  • The function 'verifyEmail' takes a string and then evaluates it using a regular expression.  This is a relatively straightforward way of enforcing whether or not the string looks basically like an email address.
  • Since our flow will also have to email the user, we'd like to be able to tell them both that they've successfully signed up, AND to check their email for an activation link.  As these are two separate messages, we've used another error-like array called messages to report such events.
  • We've created a nice random string for the activation string by hashing a unique, time based string.

With this code in place, we can now add users to our website!  However they won't be able to login until we complete the final component of our signup flow, the activation component.

4. Activation

First, let's create the email to send the user that will contain their unique activation string, and incorporate that into the signup form.  If the user successfully signed up, we want that email to be automatically sent to them.

In PHP, an email can be created and sent like this:

// Multiple email headers can be added by appending strings of the form
// 'Key: value\n' to the headers string.
// For our purposes we really only need to specify what we want the 'From' to look like:
$headers = "From: do-not-reply@your-domain.com";

// We'll write the body of the email in a similar fashion, appending to a string with newlines:
$message = "Thank you for registering at your-domain.com!\n";
$message .="If you didn't request this email, you may ignore it.\n";
$message .= "\nTo activate your account, please click on this link:\n\n";

// Now we create the link and pass both the user's email and their activation string as GET parameters.  Because will contain at least some non-url friendly characters, we urlecode it:
$message .= 'https://your-domain.com/actions/activate.php?email=' . urlencode($email) . "&key=$activation";
$message .= "\n\nPlease do not reply to this email.  It is not checked.";

// We send the mail by passing the address, subject line, body and header string to mail():
mail($email, 'Registraion Confirmation', $message, $headers)

All that's left is to code activation.php so that it can remove the activation string from a user's entry in our database so that the user may finally login!  Compared to the previous scripts, it's pretty straightforward.  However unlike the other scripts, in the course of normal operation this should not ever be accessed.  From a security perspective, the only reasonable time this would be accessed is by a valid user having clicked an activation link they received via email.  Any other attempt may be considered malicious.

<?php 
	// Check for email, key:
	if (!isset($_GET['email']) || (!isset($_GET['key']))){
		// They should both be present, and lack of them suggests something fishy
		// Handle bad accesses the way you would for the rest of your site, by redirecting, displaying a generic error, etc.  For our purposes we'll echo an error and die:
		die("Malformed URL!");
	} else {

		// Connect to DB:
		require('pdo.php');

		// Get Key and email:
		$key = $_GET['key'];
		$email = $_GET['email'];

		// Condtional on email and key
		$query = "UPDATE user SET activation=NULL WHERE email = '$email' AND activation='$key'";
		$stmt = $pdo->prepare($query);
		$stmt->execute(array($email, $key));

		// Redirect to user area/login page of your website.  For our purposes:
		echo "Thank you for activating your account!";
	}
?>

You will of course need PHP's mail to be setup, which usually means that you're running some kind of mail server available through, for instance, sendmail.  In a shared or managed hosting environment this is usually either taken care of for you, or available through your host's administration.

At this point, we've got a working, albeit basic, sign up, sign in and activation scheme!

5. Final Thoughts

Blindly implementing the above code is probably not a great idea without building upon it.  Here are some additional things to consider:

  • Use improved error checking that, for instance, enforces a high-quality password
  • Use some kind of anti-robots technique like a captcha (checkout https://github.com/yasirmturk/simple-php-captcha for a nice straightforward one)
  • "Flatten" your error messages.  While highly specific error messages may in some cases improve a user's ability to understand what happened, they may also provide attackers with an unnecessarily large amount of information.
# Reads: 5526