Table of Contents
The symbol returns the reader to the top of the Table of Contents.
1. Introduction
Over the past few years, I have been constructing and maintaining a website for a non-profit charitable organization. Until recently, the site contained non-proprietary information such as:
- A home page with hours of operation, meeting days, directions, etc.
- A menu allowing items such as the following to be accessed:
- A carousel of upcoming events.
- Rules of Games played in the organization's facility.
- The organization's officers and the officers of subordinate organizations.
- Calendars of the current and future months.
- List of Sponsors.
- The HTML pages that respond to menu selections.
All-in-all, it was a relatively simple site to construct and maintain.
However, members asked that minutes of meetings, profit and loss statements, organization documents (e.g., constitution, bylaws, etc.), drafts of documents, etc. be added. Members indicated that they wanted these items to be kept private and not be accessible to casual visitors to the site. The members' desires required a redesign of the website.
2. Requirements
There were a number of requirements that were to be levied against the new portion of the website.
- No third party software, other than those offered by the hosting platform, was to be used. This effectively eliminated all forms of software that promised out-of-the-box login solutions (including Microsoft).
- Implementation was limited to:
- HTML
- CSS
- JavaScript
- PHP - to interface between JavaScript and SQL
- SQL - for database access only
- PNG - for graphic objects
3. Overview
The website that originally had a simple directory structure now had to be partitioned into public and private portions. The public part was named "Public", the private part was named "Members", and that part that contained items common to both was named "Common". The final directory structure took on the following form:
Note that directory names are capitalized and file names are lowercase (the host uses a variant of UNIX and thus the casing).
To enable a number of changes, the website landing page was converted to a redirection page that landed the visitor on the index.html in the Public directory. This was accomplished by the following:
<!DOCTYPE html>
<html lang="en">
<head>
<title>Redirect</title>
<meta http-equiv="refresh"
content="0; URL=Public/index.html" />
</head>
<body>
</body>
</html>
4. Redesign
The first task was to revise the website menu.
The only change needed was to add a "Members Only" item to the menu.
When a visitor selects the Member Only item, he is directed to the member_login.html page.
5. HTML
This section describes member_login.html and its components.
5.1. member_login.html page
This page is made up of a main page landing <div> and five modal popup <div>s that perform most of the work. An overview of the member_login.html page follows.
Within the HTML for that page is:
<link rel="stylesheet"
href="../../Common/CSS/w3.css" />
<link rel="stylesheet"
href="../../Common/CSS/member_login.css"
type="text/css"
media="screen" />
⋮
<script src="../../Common/Scripts/SHA256.js"></script>
<script src="../../Common/Scripts/cookies.js"></script>
<script src="../../Common/Scripts/mock.js"></script>
<script src="../../Common/Scripts/member_login.js"></script>
⋮
<script>
window.onload =
function ( )
{
MemberLogin.initialize_login ( );
if ( MemberLogin.already_logged_in ( ) )
{
document.location = "../../Members/members_area.html";
}
};
</script>
There are two CSS entries:
- member_login.css - developed during this project.
- w3.css - comes from W3.CSS [^]. That CSS was used throughout the organization's website.
There are four external JavaScript files:
- SHA256.js - used to hash passwords
- cookies.js - utilities to manipulate cookies
- mock.js - creates a mock database, see below
- member_login.js - developed during this project to support the login process
When the member_login.html page first loads, the onload handler is invoked. That handler first invokes the initialize_login ( ) entry point of MemberLogin to initialize the global variables used by MemberLogin. Then the onload handler determines if the visitor has already logged in. At a successful login, a session cookie is created. If the cookie already exists, the visitor has already logged in and is now directed to members_area.html. If the cookie does not exist, the onload handler exits, thereby placing the visitor in the main_page <div>.
Because the cookie is a session cookie, if the visitor does log in but then exits the browser, the session cookie will be deleted by the browser. In that case, the visitor will be required to log on again.
5.1.1. main page <div>
main page <div> presents the visitor with:
main_page <div> is not entered if the window.onload event handler determines that the visitor is already logged in. If main page <div> is entered, it provides the visitor with the ability to either login to the member area or register.
Within the click handlers for the Login and Register buttons in the main_page <div> appear
<popup-name>.style.display='block'
This is the mechanism used to cause the various popups to appear. In the popups themselves, in the click handler for the Cancel button, appears
<this-popup-name>.style.display='none'
This causes the current popup to close, returning the visitor to the popup from whence he came.
In main_page <div>, the visitor may choose to log in to the member area, register to be able to log in, or cancel further action.
Whenever a visitor clicks Cancel, on any page of the login process, the visitor is always returned to the main_page <div> or to the original referrer of the member_login.html page.
If the visitor clicks Register, the visitor is presented with the instruction_popup <div> of the member_login.html page; if the visitor clicks Login, the visitor is presented with the login_popup <div> of the member_login.html page.
5.1.2. instructions_popup <div>
The instructions_popup <div> is a cosmetic addition that instructs the visitor how to proceed through the Registration and Login Processes. (It is surprising how many visitors contact me for help without reading these instructions.)
instructions_popup <div> presents the visitor with:
If the visitor clicks Continue, the visitor is directed to the member_id_popup <div> of the member_login.html page.
5.1.3. member_id_popup <div>
When the members only area was proposed, there was concern that casual visitors might be able to visit the organization's proprietary area. The solution was to require a login. However, just asking visitors for a username and password would not accomplish the desired effect.
The organization, at the national level, provides each member with a numbered membership card. Although the membership card is reissued annually, the member identification number is constant. Because usually only the member knows his member identification number, that number became the key to limiting usernames and passwords to members only. By a relatively simple process, the local organization's member identification numbers are downloaded from the national database and inserted into the database maintained on the organization's website.
It is that number that is required by the member_id_popup <div>.
The member_id_popup <div> presents the visitor with:
When the visitor enters a Member ID and clicks Verify, the Login Process looks up the supplied id in the database on the organization's website. There are three results to the look up: the id is recognized, the id is not recognized, or the id has already been used. In the latter two cases, the visitor is presented with one of the following:
If the verification of the id succeeds, the visitor is presented with the register_popup <div>.
5.1.4. register_popup <div>
The register_popup <div> presents the visitor with:
For both the User ID and the Password, the visitor may enter any sequence of alphanumeric characters ( 'a' through 'z'; 'A' through 'Z'; and '0' through '9'). The supplied character string must be between 6 and 64 characters in length and not include spaces or special characters.
The valication of the visitor's input is directed by the two Regex constants USER_ID_REGEX and PASSWORD_REGEX in member_login.js. By changing these constants, the input required for a username or password to be acceptable may be modified. (A note of caution: some characters may interfere with PHP or SQL execution.)
The registration process is accomplished by invoking the register ( ) entry point in MemberLogin. That method validates the data entered by the visitor and, if successful, adds the visitor's credentials to the organization's database. If registration is successful or the visitor clicked Login in the main page <div>, the visitor is directed to the login_popup <div> of the member_login.html page.
5.1.5. login_popup <div>
The login_popup <div> presents the visitor with:
The Login Process is accomplished by invoking the login ( ) entry point in MemberLogin. That method validates the credentials entered by the visitor against the credentials in the organization's database. The visitor is given three attempts to successfully login. When he does, he is admitted to the members area.
For any number of reasons, a user may wish to change his password. If the user clicks Forgot password?, the user is directed to the reset_popup <div> of the member_login.html page.
5.1.6. reset_popup <div>
The reset_popup <div> presents the visitor with:
Resetting the password is accomplished by invoking the reset_password ( ) entry point in MemberLogin. That method insures that both the user id and member id have been used.
6. Database
The database that contains member login data is maintained on the hosting service's database server with the name Members_DB. The database contains two tables and six procedures.
6.1. Tables
6.1.1. member_id
The member_id table contains all member identification numbers that will be accepted during the registration process. The values in this table are derived from the national organization's data, maintained on their website.
Unfortunately, in my case, the national organization was unable (or unwilling) to create a web service [^] that would provide validation of member identification numbers. As a result, it is necessary to download a CSV [^] database, manipulate it, and upload the manipulated values (member ids) to the member_id table.
6.1.2. member_user_salt_password
When a visitor successfully registers, a record (row) in the member_user_salt_password table is created. User credentials are maintained in this table. Although the member_user_salt_password table is accessed through the user_id, the attribute member_id remains in the table for the purposes of resetting the password. The user wishing to change a password must know both the user_id and the member_id.
6.2. Procedures
Database procedures are coded in SQL. Each database procedure is accessed through a PHP function, itself invoked by a JavaScript method found in the module MemberLogin. The interactions between JavaScript, PHP, SQL, and the database can be found here.
6.2.1. delete_member
delete_member is referenced by MemberLogin.member_deleted to delete an existing member from Member_DB.member_user_salt_password table. MemberLogin.member_deleted returns true, if the deletion from Member_DB.member_user_salt_password is successful; otherwise, false.
-
PROCEDURE `delete_member` ( IN `mid` VARCHAR ( 16 ),
IN `uid` VARCHAR ( 64 ))
NO SQL
BEGIN
DELETE
FROM member_user_salt_password
WHERE member_id = mid
AND user_id = uid;
END
6.2.2. insert_a_member
insert_a_member is referenced by MemberLogin.insert_a_member to insert a new member into the Member_DB.member_user_salt_password table. MemberLogin.insert_a_member returns true, if the insertion is successful; otherwise, false.
-
PROCEDURE `insert_a_member` ( IN `member_id` VARCHAR ( 16 ),
IN `user_id` VARCHAR ( 64 ),
IN `salt` VARCHAR ( 12 ),
IN `hashed_password` VARCHAR ( 64 ) )
NO SQL
BEGIN
INSERT INTO member_user_salt_password ( member_id,
user_id,
salt,
hashed_password )
VALUES ( member_id,
user_id,
salt,
hashed_password );
END
6.2.3. member_exists
member_exists is referenced by MemberLogin.member_id_verified to verify that a visitor-supplied member ID exists in the Member_DB.member_id table. MemberLogin.member_id_verified returns true, if the supplied id is found in the collection of member ids in the Member_DB.member_id table; otherwise, false
-
PROCEDURE `member_exists` ( IN `id` VARCHAR ( 16 ) )
NO SQL
BEGIN
SELECT * FROM member_id WHERE member_id = id;
END
6.2.4. member_id_already_in_use
member_id_already_in_use is referenced by MemberLogin.member_id_already_used to determine if a visitor-supplied member ID already exists in the Member_DB.member_user_salt_password table. MemberLogin.member_id_already_used returns true, if the supplied id is found; otherwise, false.
-
PROCEDURE `member_id_already_in_use` ( IN `id` VARCHAR ( 16 ) )
NO SQL
BEGIN
SELECT member_id
FROM member_user_salt_password
WHERE member_id = id;
END
6.2.5. retrieve_salt_hash
retrieve_salt_hash is referenced by MemberLogin.salt_hash_retrieved to retrieve the salt and hashed password associated with the specified user id from the Member_DB.member_user_salt_password table. MemberLogin.salt_hash_retrieved returns true, if the retrieval is successful; otherwise, false. MemberLogin.salt_hash_retrieved returns the salt and hashed password in the global variables stored_salt and stored_hashed_password, respectively.
-
PROCEDURE `retrieve_salt_hash` ( IN `id` VARCHAR ( 64 ) )
NO SQL
BEGIN
SELECT salt,
hashed_password
FROM member_user_salt_password
WHERE user_id = id;
END
6.2.6. user_already_exists
user_already_exists is referenced by MemberLogin.user_already_exists to determine if the specified user id is found in the Member_DB.member_user_salt_password table. MemberLogin.user_already_exists returns true, if the supplied user id is found in Member_DB.member_user_salt_password; otherwise, false.
-
PROCEDURE `user_already_exists` ( IN `id` VARCHAR ( 64 ) )
NO SQL
BEGIN
SELECT user_id
FROM member_user_salt_password
WHERE user_id = id;
END
7. JavaScript
JavaScript is truly the "glue" that holds all of the components of the Login Project together.
There are two important external JavaScript files.
- SHA256.js
- member_login.js
7.1. SHA256.js
SHA256.js was derived from the Web Toolkit [^] snippet library. That script contains a generator for an almost-unique 256-bit (32-byte) signature for a given text. The reader is referred to SHA-2 [^] for details. SHA256.js is referenced by a method contained in member_login.js.
The result of applying the SHA256 algorithm is not an encryption. The result cannot be "decrypted". It is referred to as a "one-way" or "trap door" operation. This makes it well suited for the generation of hashed passwords. The function in MemberLogin that generates the hash is
hashed_password ( password,
salt )
where password is the plain text password to be hashed and salt is a string containing a random string obtained from invoking the function string_salt.
hash = SHA256 ( salt + password );
Prefixing (or suffixing) a salt [^] to a password is a standard practice when generating hashed passwords. hashed_password returns hash and the invoking function saves both salt and hash in the database.
7.2. member_login.js
The contents of member_login.js are used extensively in the Login Process. member_login.js is constructed as a module of utility functions that support member login. The module is named MemberLogin. MemberLogin exports the following public properties (entry points):
- already_logged_in
- clear_error_message
- hashed_password
- initialize_login
- insert_a_member
- is_eligible
- login_popup
- login
- password_hide_show
- random_string
- register_popup
- register
- reset_password
- reset_popup
- return_to_referrer
- session_variables
- set_keyboard_focus_to_id
- string_salt
There are three major popups in the Login Project.
- Login
- Register
- Reset Password
There are two ancillary popups in the Login Project.
Each of these popups is found in a separate popup <div> of the member_login.html page and each popup (except Instructions that is self contained) invokes one or more functions in member_login.js.
In addition, there are six PHP functions
- delete_a_member
- insert_a_member
- member_exists
- member_id_already_in_use
- retrieve_salt_hash
- user_exists
7.2.1. login popup
7.2.2. register popup
7.2.3. reset_password popup
7.2.4. member_id popup
7.2.5. instructions popup
7.3. PHP
The PHP functions provide an interface between JavaScript and the database SQL procedures. The interactions are depicted in the following diagram.
In all the PHP functions discussed in the following sections, the required connection string is made up of the database server name, database user name, database password, and database name. For obvious reasons, these data items will not be divulged here. Rather, the following will be substituted:
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
The hosting service will normally prohibit direct access to PHP files from the Internet.
7.3.1. delete_a_member ( )
There is no update available in the Login Project. Rather, when user data must be revised, the revision takes place as a deletion followed by an insertion. For example in the JavaScript reset_password ( ) method appears
if ( !member_deleted ( ) )
{
set_error_message (
reset_error_message,
"Either the Member ID or User ID is not recognized" );
return;
}
session_variables.user_id = user_id;
session_variables.salt = string_salt ( 12, "aA#" );
session_variables.hash = hashed_password (
password,
session_variables.salt );
session_variables.password = password;
session_variables.already_logged_in = false;
if ( insert_a_member ( ) )
{
login_popup.style.display='block';
reset_popup.style.display='none';
}
else
{
set_error_message ( reset_error_message,
"Failed to reset password" );
return;
}
Here, the record of the member whose password is being revised is first deleted and then a new member record is inserted.
From the earlier figure, the JavaScript member_deleted ( ) invokes the PHP delete_a_member ( ) and the JavaScript insert_a_member ( ) invokes the PHP insert_a_member ( ).
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['member_id']));
$r = strval(htmlspecialchars($_GET['user_id']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( !$connection )
{
die ( "Connection failed: " . mysqli_connect_error ( ) );
}
$sql = "CALL delete_member('".$q."','".$r."')";
mysqli_query ( $connection, $sql );
echo mysqli_affected_rows ( $connection );
mysqli_close ( $connection );
?>
7.3.2. insert_a_member ( )
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['member_id']));
$r = strval(htmlspecialchars($_GET['user_id']));
$s = strval(htmlspecialchars($_GET['salt']));
$t = strval(htmlspecialchars($_GET['hashed_password']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( !$connection )
{
die ( "Connection failed: " . mysqli_connect_error ( ) );
}
$sql = "CALL insert_a_member('".$q."','".$r."','".$s."','".$t."')"
if ( mysqli_query ( $connection, $sql ) )
{
echo "OK"
}
else
{
echo mysqli_error ( $connection );
}
mysqli_close ( $connection );
? >
An example of the inserted data (less the member_id) is:
user_id salt hashed_password
gggustafson LtNkIOr2ooLR bb89891c2484965cc638d5eed159bdb795e0c48064d9769698
7.3.3. member_exists ( )
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['q']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( !$connection )
{
die ( "Connection failed: " . mysqli_connect_error ( ) );
}
$sql = "CALL member_exists ( '" .$q. "' )"
$result = mysqli_query ( $connection, $sql );
if ( mysqli_num_rows ( $result ) > 0 )
{
while ( $row = mysqli_fetch_assoc ( $result ) )
{
echo $row [ "member_id" ];
}
}
else
{
echo "0"
}
mysqli_close ( $connection );
? >
7.3.4. member_id_already_in_use ( )
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['q']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( !$connection )
{
die ( "Connection failed: " . mysqli_connect_error ( ) );
}
$sql = "CALL member_id_already_in_use ( " .$q. " )";
$result = mysqli_query ( $connection, $sql );
if ( mysqli_num_rows ( $result ) > 0 )
{
while ( $row = mysqli_fetch_assoc ( $result ) )
{
echo $row [ "member_id" ];
}
}
else
{
echo "0";
}
mysqli_close ( $connection );
?>
7.3.5. retrieve_salt_hash ( )
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['q']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( mysqli_connect_errno ( ) )
{
printf ( "Connect failed: %s\r\n",
mysqli_connect_error ( ) );
exit();
}
$sql = "CALL retrieve_salt_hash ( '" .$q. "' )";
if ( $result = mysqli_query ( $connection, $sql ) )
{
if ( $result- >num_rows > 0 )
{
while ( $row = $result- >fetch_row ( ) )
{
echo ( $row [ 0 ] ." ". $row [ 1 ] );
}
}
else
{
echo "0";
}
$result- >close();
}
else
{
echo "0";
}
mysqli_close ( $connection );
? >
7.3.6. user_exists ( )
<?php
ini_set ( "display_errors", 1 );
error_reporting ( E_ALL );
$q = strval(htmlspecialchars($_GET['q']));
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
$connection = mysqli_connect ( $servername,
$username,
$password,
$database );
if ( !$connection )
{
die ( "Connection failed: " . mysqli_connect_error ( ) );
}
$sql = "CALL user_already_exists ( '" .$q. "' )";
$result = mysqli_query ( $connection, $sql );
if ( mysqli_num_rows ( $result ) > 0 )
{
while ( $row = mysqli_fetch_assoc ( $result ) )
{
echo $row [ "user_id" ];
}
}
else
{
echo "0";
}
mysqli_close ( $connection );
? >
8. Using the Code
The software contained in the download is a version that mocks a database. There are a number of items that must be addressed to modify it to execute in a production environment.
8.1. Modifying the PHP files
As discussed earlier, the supplied PHP files contain dummy values for the connection string
$servername = "server-name"
$username = "database-user"
$password = "database-password"
$database = "database-name"
Each value on the right-hand side of these assignment statements must be replaced by actual values in the executing environment.
8.2. Turning off Mocking
See the section Mocking the Database for details.
8.3. Insuring Login is Required
It is imperative that a visitor who is not a member never be able to access pages in the members only area of the website. This can be accomplished by using the following template for every page in the members only area.
The smallest page in the download is under_construction.html. That page contains the important <div>s and <script>s discussed above.
<!DOCTYPE html >
<html lang="en">
<head>
<title>Under Construction</title>
<meta http-equiv="Content-type"
content="text/html; charset=UTF-8" />
<meta name="viewport"
content="width=device-width, initial-scale=1.0" />
<link rel="icon"
type="image/x-icon"
href="../Common/Images/favicon.ico"/>
<link rel="stylesheet"
href="../Common/CSS/w3.css" />
<link rel="stylesheet"
href="../Common/CSS/members_area.css" />
</head>
<body class="member-area">
<div id="contents"
class="content output w3-panel centered"
style="display:none;">
<img src="../Common/Images/under_construction.png"
alt="Under Construction"
width="300px"
height="300px"
style="margin-top:10px;"
/>
</div>
<script src="../Common/Scripts/cookies.js"></script>
<script src="../Common/Scripts/member_login.js"></script>
<script>
window.onload =
function ( )
{
if ( MemberLogin.already_logged_in ( ) )
{
document.getElementById ( 'contents' ).style.display =
"block";
}
else
{
document.location = "./MemberLogin/member_login.html";
}
};
</script>
</body>
</html>
Points of interest:
9. References
10. Download
The download, at the top of this article, contains all of the files needed to implement the login mechanism described in this article (as well as this article). Its directory structure is:
I suggest that the ZIP file be downloaded and then extracted into a newly created directory named "Login".
10.1. Mocking the Database
The file member_login.js contains all of the functions necessary to perform logins. However, since many developers do not have PHP and SQL installed on their machines, a database mock has been included in the file mock.js. This file is included in member_login.html by
<script src="../../Common/Scripts/mock.js"></script>
Within each function that accesses a PHP function appears
if ( MOCK )
{
return ( Mock.<mock-code-to-execute> );
}
else
Wherever this fragment appears it begins in column one and is followed by a block of code that is executed in non-mocking mode. If mocking is to be totally removed, the if ( MOCK ) block can be removed, leaving only the code block following the else. For purists, the braces surrounding the else block could also be removed.
The constant MOCK is declared at the top of member_login.js
var MOCK = true;
If mocking is just to be disabled, then this constant need only be set to false.
If mocking is desired, insure that the constant MOCK is set true in the member_login.js file. When members_area.html is entered, the cookie LOGGEDIN_COOKIE_NAME will not exist and the visitor will be directed to member_login.html.
The mocking software accepts odd number from 1 through 19, inclusive, as member_ids. These values may be modified by modifying the initialize_mock_login_project_variables function in the mock.jsfile.
10.2. members_area.html
When testing the login mechanism described in this article, one needs only open members_area.html in the Members directory. If the cookie LOGGEDIN_COOKIE_NAME does not exist, the visitor will be redirected to member_login.html. If the cookie does exist, the following will be displayed.
To allow multiple testing of the login mechanism, a Delete Cookie button is included. To remove the button, code in the members_area.html surrounded by
<!-- start for debugging -->
:
<!-- end for debugging -->
should be removed (including the HTML comments).
11. Conclusion
This article has provided the code necessary to implement a website login mechanism without using third-party software.
12. Development Environment
The Login Project was developed in the following environment:
Microsoft Windows 7 Professional SP 1 |
Microsoft Visual Studio 2008 Professional SP1 |
Microsoft Visual C# 2008 |
Microsoft .Net Framework Version 3.5 SP1 |
13. History
07/05/2021 | Original article |