How to authenticate users with PHP and flat JSON files
Yesterday, I wrote about how to build dynamic web apps using a static site generator and a tiny PHP file.
In it, I noted…
I’m a huge fan of flat JSON files. In my experience, they’ve proven far more resilient, maintainable, and performant than using a proper database.
One reader asked me how I setup authentication with this technique, so today I wanted to share how that works. Let’s dig in!
Storing user data
User data lives in a private directory that can only be accessed on the server and cannot be reached with an HTTP request. You can configure this with an .htaccess
file in apache.
I use the file_put_contents()
function to write data to JSON files, and file_get_contents()
to retrieve it. I also use the json_decode()
and json_encode()
functions to convert strings to JSON and back again.
Creating a new user
There was a time where you needed a library to hash and validate passwords with PHP. But the language now has built-in methods to do that: password_hash()
and password_verify()
.
They actually use a stronger hashing algorithm than WordPress does.
When I create a new user, I use the password_hash()
method to hash their provided password. I create an object with their hashed password, convert it to a string, and save it a JSON file with their username or email address as the file name.
It’s easy and fast, and way less confusing than writing a SQL query!
<?php
// Create the user
$pw = 'the user provided password';
$user = array(
'pw' => password_hash($pw, PASSWORD_DEFAULT)
);
// Save user to a flat JSON file
file_put_contents('/private-directory/users/AwesomeUsername.json', json_encode($user));
Logging in
To log a user in, I open the JSON file with their username and convert it to JSON. Then, I use the password_verify()
function to check if the provided password matches the hashed version stored in the JSON file.
If the password isn’t correct, I can throw an error and let the user know.
<?php
$user = json_decode(file_get_contents('/private-directory/users/AwesomeUsername.json'));
$is_valid = password_verify($provided_pw, $user['pw']);
if (!$is_valid) {
// throw an error
}
Storing sessions
If the password is valid, I generate a random string to act as a session ID, and set a cookie with that string as its value.
I set my sessions to last two weeks, but you can of course use any duration you want. I include the expiration date as part of the $token
string. This makes clearing old sessions easier, as we’ll look at shortly.
For security reasons, I set the cookie so that it can only be read by the server. JavaScript cannot access it.
<?php
// Create a token details
$expires = time() + (60 * 60 * 24 * 14);
$token = '_' . $expires . '_' . bin2hex(openssl_random_pseudo_bytes(60));
// Set cookie
$cookie = setcookie('session_token', $token, $expires, '/', '', true, true);
After the cookie is set, I save a JSON file named after the $token
with the user’s username and the expiration date.
<?php
$token_val = array(
'user' => $username,
'expires' => $expires,
);
file_put_contents('/private-directory/tokens/' . $token . '.json', json_encode($token_val));
Authenticating sessions
Whenever a page loads, I can now use the session_token
cookie to check if the user is logged in and get their details.
I get the value of the session_token
cookie, and use it to get the matching token file. That file gives me access to the current user’s username.
<?php
// Check for a session cookie
$token_id = isset($_COOKIE['session_token']) ? $_COOKIE['session_token'] : null;
if (!$token_id) {
// the user is not logged in...
}
// Get the token details
$token = get_file_contents('/private-directory/tokens/' . $token_id . '.json');
if (empty($token)) {
// the user is not logged in...
// Remove the cookie, too
}
// Get the username
$username = $token['user'];
Once I have this info, I can do anything else I need to check in my app.
Sometimes just knowing they’re logged in is enough. Other time, I may want to show conditional content based on the user, which may require additional checks or API calls.
Cleaning up old sessions
My session_token
cookies automatically expire, but the corresponding JSON files do not.
I have a cron task set on my server to automatically delete expired token JSON files each night. It is, once again, a PHP file that runs some tasks.
<?php
// Loop through tokens
foreach (new DirectoryIterator(dirname(__FILE__) . '/tokens') as $fileInfo) {
// If a . file, skip
if ($fileInfo->isDot()) continue;
// Get expiration date
$expires = intval(explode('_', $fileInfo->getFilename())[1]);
// If token/key has expired, remove it
if ($expires < time()) {
unlink($fileInfo->getPathname());
}
}