When last we left off I had created the table we’re going to use to store all user account information.
Now we’re going to build up a registration form and a couple of PHP scripts to enable us to register new accounts. So, breaking down the things that I want:
- A registration page with a form.
- Client and server-side validation of the form.
- A script to check for existing usernames.
- A script to insert a new row into the database upon submission of a valid form.
Form
The form is pretty straightforward. We want to get the desired username for the player, an email address so that we can use it for password issues, and the password for the account. The ID, salt, and joined timestamp columns in our accounts table will be taken care of by the ‘register.php’ script action later.
<form id="registerForm" action="register.php" method="post">
<fieldset>
<legend>Please complete all fields to register</legend>
<p>
<label for="username">Username</label>
<input id="username" name="username" type="text" maxlength="16"/>
</p>
<p>
<label for="email">Email</label>
<input id="email" name="email" type="text"/>
</p>
<p>
<label for="password">Password</label>
<input id="password" name="password" type="password"/>
</p>
<p>
<label for="verifyPassword">Verify Password</label>
<input id="verifyPassword" name="verifyPassword" type="password"/>
</p>
<p>
<input class="submit" type="submit" value="Submit"/>
</p>
</fieldset>
</form>
Validation
I was 30 minutes or so into getting a hand of JavaScript and writing my own validation scripts when I realized how much of an idiot I was and that I should check for existing solutions. I knew about jQuery so I checked there first, and within minutes I was knee-deep into the jQuery.validate.js API documentation. Turns out this is perfect for my needs, and after an hour or so of tinkering around I had a good set of rules in place for our form:
- The username must be 3 to 16 characters in length. No numbers or punctuation. The username must be unique.
- The email address must be valid. I may look into actual verification of this later, but for now I’ll just trust the user.
- The password must be at least 6 characters in length, and must be verified a second time.
$(document).ready(function() {
jQuery.validator.addMethod("validUsername", function(value, element) {
return this.optional(element) || /^[a-zA-Z]{3,16}$/i.test(value);
}, "Your username must be from 3 to 16 characters in length. No numbers or punctuation are allowed.");
$("#registerForm").validate({
success: function(label) {
label.addClass("valid").text("Ok");
},
onkeyup: false,
rules: {
username: {
required: true,
validUsername: true,
remote: {
url: "username.php",
type: "post",
},
},
email: {
required: true,
email: true,
},
password: {
required: true,
minlength: 6,
},
verifyPassword: {
required: true,
equalTo: "#password",
},
},
messages: {
username: {
required: "A username is required.",
minlength: jQuery.format("At least {0} characters are required."),
remote: "This username is already in use.",
},
email: {
required: "A valid email is required for password recovery.",
},
password: {
required: "A password is required.",
minlength: jQuery.format("At least {0} characters are required."),
},
verifyPassword: {
required: "The password must be verified.",
equalTo: "Please enter the same password as above.",
},
},
});
});
Verify Username
In the above validation rules you may have noticed the ‘remote:’ key under username. What this does is send out an AJAX request to the specified script (usernames.php) via POST with the username as the default param. Then, depending on the return value of true/false will determine if the field is validated. All that the script does is connect to my database for Space Horde via PHP’s PDO methods, run a SELECT statement to see if any rows are returned with the given username, and return true or false based on that.
<?php
require_once('../credentials.php');
if(array_key_exists('username', $_POST))
{
$username = trim(strip_tags($_POST['username']));
try
{
$dbh = new PDO('mysql:host=' . $myDBHost . ';dbname=' . $myDBName, $myDBUser, $myDBPass, array(PDO::ATTR_PERSISTENT => true));
$stmt = $dbh->prepare('SELECT username FROM accounts WHERE username=:username');
$stmt->bindValue(':username', $username);
if($stmt->execute() && $stmt->fetch())
echo 'false';
else
echo 'true';
}
catch(PDOException $e)
{
die($e->getMessage());
}
}
else
{
die('Invalid POST data.');
}
$dbh = null;
?>
Create Account
Once our form data is validated and submitted it will all be sent to our final script, register.php, via the POST method. There we pick it up and do a few extra things. First, we do not want to store the user’s password as raw text as anyone who gets a hold of the database will immediately have access to the passwords. Instead, we’ll store the password as a hashed version of the hashed password + a hashed random salt. A hashed hash plus a hash. Ya, I know.
$salt = hash("sha256", mt_rand());
$passwordHash = hash("sha256", $password);
$passwordAndSaltHash = hash("sha256", $password . $salt);
We use PHP’s Mersenne Twister implementation to grab a good random number and then hash that into a 64 character hex string via the SHA256 algo with the hash function. This will be our unique salt value for this user. We then hash the raw password in the same way giving us another 64 character hex string. Finally we concatenate the password and salt hashes together and and hash that into a final hex string. This combination is what we’ll actually store as the user’s password, along with the salt value.
The idea here is that we never want to know the user’s password once it has been saved in our database. Verification of the user will happen by sending them the stored salt value on login which the client will use to generate a hash in the same way we created ‘$passwordAndSaltHash’ above. That is sent to the server and compared to the store value. If they match, the user is authenticated.
The rest of our script generates a date stamp, runs the same basic validation checks on the posted data to ensure the data is correct (in the case that JavaScript is disabled client-side), and then runs a SQL INSERT query to create the new row. Then we’re done! We can register users. :)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Space Horde - Account Registration</title>
</head>
<body>
<?php
require_once("../credentials.php");
if(array_key_exists("username", $_POST) && array_key_exists("email", $_POST) && array_key_exists("password", $_POST))
{
$username = trim(strip_tags($_POST["username"]));
$email = trim(strip_tags($_POST["email"]));
if(strlen($email) == 0)
die("A valid email address is required for password recovery.");
$password = $_POST["password"];
if(strlen($password) < 6)
die("Your password must be at least 6 characters long.");
$salt = hash("sha256", mt_rand());
$passwordHash = hash("sha256", $password);
$passwordAndSaltHash = hash("sha256", $password . $salt);
$date = date("Y-m-d");
try
{
$dbh = new PDO("mysql:host=" . $myDBHost . ";dbname=" . $myDBName, $myDBUser, $myDBPass, array(PDO::ATTR_PERSISTENT => true));
if(!validateUsername($username))
die("Your username must be from 3 to 16 characters long. No numbers or punctuation are allowed.");
if(!uniqueUsername($dbh, $username))
die("The username " . $username . " is already in use.");
$stmt = $dbh->prepare("INSERT INTO accounts (username, email, password, salt, joined) VALUES (:username, :email, :password, :salt, :joined)");
$stmt->bindValue(":username", $username);
$stmt->bindValue(":email", $email);
$stmt->bindValue(":password", $passwordAndSaltHash);
$stmt->bindValue(":salt", $salt);
$stmt->bindValue(":joined", $date);
if($stmt->execute())
echo "Thank you for registering, " . $username . "!";
else
{
echo "Unable to register your account.";
}
}
catch(PDOException $e)
{
die($e->getMessage());
}
$dbh = null;
}
function uniqueUsername($dbh, $username)
{
try
{
$stmt = $dbh->prepare('SELECT username FROM accounts WHERE username=:username');
$stmt->bindValue(':username', $username);
if($stmt->execute() && $stmt->fetch())
return false;
else
return true;
$dbh = null;
}
catch(PDOException $e)
{
die($e->getMessage());
}
}
function validateUsername($username)
{
return preg_match("/^\b[a-zA-Z]{3,16}\b/", $username) == 1;
}
?>
</body>
</html>
It isn’t exactly pretty, but it’s functional. Try it here. Next time we’ll look at authenticating a user from a C# application.