As the name implies, middleware sits in the middle of a sequence of function or method calls. Accordingly, middleware is well suited for the task of "gate keeper". You can easily implement an Access Control List (ACL) mechanism with a middleware class that reads the ACL, and allows or denies access to the next function or method call in the sequence.
level and a status. In this illustration, the level is defined as follows:'levels' => [0, 'BEG', 'INT', 'ADV']
0 could indicate they've initiated the membership signup process, but have not yet been confirmed. A status of 1 could indicate their e-mail address is confirmed, but they have not paid the monthly fee, and so on.'pages' => [0 => 'sorry', 'logout' => 'logout', 'login' => 'auth',
1 => 'page1', 2 => 'page2', 3 => 'page3',
4 => 'page4', 5 => 'page5', 6 => 'page6',
7 => 'page7', 8 => 'page8', 9 => 'page9']level and status. The generic template used in the configuration array might look like this:status => ['inherits' => <key>, 'pages' => [level => [pages allowed], etc.]]
Acl class. As before, we use a few classes, and define constants and properties appropriate for access control:namespace Application\Acl;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface;
use Application\MiddleWare\ { Constants, Response, TextStream };
class Acl
{
const DEFAULT_STATUS = '';
const DEFAULT_LEVEL = 0;
const DEFAULT_PAGE = 0;
const ERROR_ACL = 'ERROR: authorization error';
const ERROR_APP = 'ERROR: requested page not listed';
const ERROR_DEF =
'ERROR: must assign keys "levels", "pages" and "allowed"';
protected $default;
protected $levels;
protected $pages;
protected $allowed; __construct() method, we break up the assignments array into $pages, the resources to be controlled, $levels, and $allowed, which are the actual assignments. If the array does not include one of these three sub-components, an exception is thrown:public function __construct(array $assignments)
{
$this->default = $assignments['default']
?? self::DEFAULT_PAGE;
$this->pages = $assignments['pages'] ?? FALSE;
$this->levels = $assignments['levels'] ?? FALSE;
$this->allowed = $assignments['allowed'] ?? FALSE;
if (!($this->pages && $this->levels && $this->allowed)) {
throw new InvalidArgumentException(self::ERROR_DEF);
}
}$allowed, the inherits key can be set to another key within the array. If so, we need to merge its values with the values currently under examination. We iterate through $allowed in reverse, merging any inherited values each time through the loop. This method, incidentally, also only isolates rules that apply to a certain status and level:protected function mergeInherited($status, $level)
{
$allowed = $this->allowed[$status]['pages'][$level]
?? array();
for ($x = $status; $x > 0; $x--) {
$inherits = $this->allowed[$x]['inherits'];
if ($inherits) {
$subArray =
$this->allowed[$inherits]['pages'][$level]
?? array();
$allowed = array_merge($allowed, $subArray);
}
}
return $allowed;
}400 code:public function isAuthorized(RequestInterface $request)
{
$code = 401; // unauthorized
$text['page'] = $this->pages[$this->default];
$text['authorized'] = FALSE;
$page = $request->getUri()->getQueryParams()['page']
?? FALSE;
if ($page === FALSE) {
$code = 400; // bad requeststatus and level. We are then in a position to call mergeInherited(), which returns an array of pages accessible to this status and level:} else {
$params = json_decode(
$request->getBody()->getContents());
$status = $params->status ?? self::DEFAULT_LEVEL;
$level = $params->level ?? '*';
$allowed = $this->mergeInherited($status, $level);$allowed array, we set the status code to a happy 200, and return an authorized setting along with the web page that corresponds to the page code requested:if (in_array($page, $allowed)) {
$code = 200; // OK
$text['authorized'] = TRUE;
$text['page'] = $this->pages[$page];
} else {
$code = 401; }
}$body = new TextStream(json_encode($text)); return (new Response())->withStatus($code) ->withBody($body); } }
After that, you will need to define Application\Acl\Acl, which is discussed in this recipe. Now move to the /path/to/source/for/this/chapter folder and create two directories: public and
pages. In pages, create a series of PHP files, such as page1.php, page2.php, and so on. Here is an example of how one of these pages might look:
<?php // page 1 ?> <h1>Page 1</h1> <hr> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. etc.</p>
You can also define a menu.php page, which could be included in the output:
<?php // menu ?> <a href="?page=1">Page 1</a> <a href="?page=2">Page 2</a> <a href="?page=3">Page 3</a> // etc.
The logout.php page should destroy the session:
<?php $_SESSION['info'] = FALSE; session_destroy(); ?> <a href="/">BACK</a>
The auth.php page will display a login screen (as described in the previous recipe):
<?= $auth->getLoginForm($action) ?>
You can then create a configuration file that allows access to web pages depending on level and status. For the sake of illustration, call it chap_09_middleware_acl_config.php and return an array that might look like this:
<?php
$min = [0, 'logout'];
return [
'default' => 0, // default page
'levels' => [0, 'BEG', 'INT', 'ADV'],
'pages' => [0 => 'sorry',
'logout' => 'logout',
'login' => 'auth',
1 => 'page1', 2 => 'page2', 3 => 'page3',
4 => 'page4', 5 => 'page5', 6 => 'page6',
7 => 'page7', 8 => 'page8', 9 => 'page9'],
'allowed' => [
0 => ['inherits' => FALSE,
'pages' => [ '*' => $min, 'BEG' => $min,
'INT' => $min,'ADV' => $min]],
1 => ['inherits' => FALSE,
'pages' => ['*' => ['logout'],
'BEG' => [1, 'logout'],
'INT' => [1,2, 'logout'],
'ADV' => [1,2,3, 'logout']]],
2 => ['inherits' => 1,
'pages' => ['BEG' => [4],
'INT' => [4,5],
'ADV' => [4,5,6]]],
3 => ['inherits' => 2,
'pages' => ['BEG' => [7],
'INT' => [7,8],
'ADV' => [7,8,9]]]
]
];Finally, in the public folder, define index.php, which sets up autoloading, and ultimately calls up both the Authenticate and Acl classes. As with other recipes, define configuration files, set up autoloading, and use certain classes. Also, don't forget to start the session:
<?php
session_start();
session_regenerate_id();
define('DB_CONFIG_FILE', __DIR__ . '/../../config/db.config.php');
define('DB_TABLE', 'customer_09');
define('PAGE_DIR', __DIR__ . '/../pages');
define('SESSION_KEY', 'auth');
require __DIR__ . '/../../Application/Autoload/Loader.php';
Application\Autoload\Loader::init(__DIR__ . '/../..');
use Application\Database\Connection;
use Application\Acl\ { Authenticate, Acl };
use Application\MiddleWare\ { ServerRequest, Request, Constants, TextStream };Best practice
It is a best practice to protect your sessions. An easy way to help protect a session is to use session_regenerate_id(), which invalidates the existing PHP session identifier and generates a new one. Thus, if an attacker were to obtain the session identifier through illegal means, the window of time in which any given session identifier is valid is kept to a minimum.
You can now pull in the ACL configuration, and create instances for Authenticate as well as Acl:
$config = require __DIR__ . '/../chap_09_middleware_acl_config.php'; $acl = new Acl($config); $conn = new Connection(include DB_CONFIG_FILE); $dbAuth = new DbTable($conn, DB_TABLE); $auth = new Authenticate($dbAuth, SESSION_KEY);
Next, define incoming and outbound request instances:
$incoming = new ServerRequest(); $incoming->initialize(); $outbound = new Request();
If the incoming request method was post, process the authentication calling the login() method:
if (strtolower($incoming->getMethod()) == Constants::METHOD_POST) {
$body = new TextStream(json_encode(
$incoming->getParsedBody()));
$response = $auth->login($outbound->withBody($body));
}If the session key defined for authentication is populated, that means the user has been successfully authenticated. If not, we program an anonymous function, called later, which includes the authentication login page:
$info = $_SESSION[SESSION_KEY] ?? FALSE;
if (!$info) {
$execute = function () use ($auth) {
include PAGE_DIR . '/auth.php';
};Otherwise, you can proceed with the ACL check. You first need to find, from the original query, which web page the user wants to visit, however:
} else {
$query = $incoming->getServerParams()['QUERY_STRING'] ?? '';You can then reprogram the $outbound request to include this information:
$outbound->withBody(new TextStream(json_encode($info))); $outbound->getUri()->withQuery($query);
Next, you'll be in a position to check authorization, supplying the outbound request as an argument:
$response = $acl->isAuthorized($outbound);
You can then examine the return response for the authorized parameter, and program an anonymous function to include the return page parameter if OK, and the sorry page otherwise:
$params = json_decode($response->getBody()->getContents());
$isAllowed = $params->authorized ?? FALSE;
if ($isAllowed) {
$execute = function () use ($response, $params) {
include PAGE_DIR .'/' . $params->page . '.php';
echo '<pre>', var_dump($response), '</pre>';
echo '<pre>', var_dump($_SESSION[SESSION_KEY]);
echo '</pre>';
};
} else {
$execute = function () use ($response) {
include PAGE_DIR .'/sorry.php';
echo '<pre>', var_dump($response), '</pre>';
echo '<pre>', var_dump($_SESSION[SESSION_KEY]);
echo '</pre>';
};
}
}Now all you need to do is to set the form action and wrap the anonymous function in HTML:
$action = $incoming->getServerParams()['PHP_SELF']; ?> <!DOCTYPE html> <head> <title>PHP 7 Cookbook</title> <meta http-equiv="content-type" content="text/html;charset=utf-8" /> </head> <body> <?php $execute(); ?> </body> </html>
To test it, you can use the built-in PHP web server, but you will need to use the -t flag to indicate that the document root is public:
cd /path/to/source/for/this/chapter php -S localhost:8080 -t public
From a browser, you can access the http://localhost:8080/ URL.
If you try to access any page, you will simply be redirected back to the login page. As per the configuration, a user with status = 1, and level = BEG can only access page 1 and log out. If, when logged in as this user, you try to access page 2, here is the output:

This example relies on $_SESSION as the sole means of user authentication once they have logged in. For good examples of how you can protect PHP sessions, please see Chapter 12, Improving Web Security, specifically the recipe entitled Safeguarding the PHP session.