In this article by Doug Bierer, author of the book PHP 7 Programming Cookbook, we will cover the following topics:
(For more resources related to this topic, see here.)
As often happens in the IT industry, terms get invented, and then used and abused. The term middleware is no exception. Arguably the first use of the term came out of the Internet Engineering Task Force (IETF) in the year 2000. Originally, the term was applied to any software which operates between the transport (that is, TCP/IP) and the application layer. More recently, especially with the acceptance of PHP Standard Recommendation number 7 (PSR-7), middleware, specifically in the PHP world, has been applied to the web client-server environment.
One very important usage of middleware is to provide authentication. Most web-based applications need the ability to verify a visitor via username and password. By incorporating PSR-7 standards into an authentication class, you will make it generically useful across the board, so to speak, being secure that it can be used in any framework that provides PSR-7-compliant request and response objects.
namespace ApplicationAcl;
use PsrHttpMessage { RequestInterface, ResponseInterface };
interface AuthenticateInterface
{
public function login(RequestInterface $request) :
ResponseInterface;
}
Note that by defining a method that requires a PSR-7-compliant request, and produces a PSR-7-compliant response, we have made this interface universally applicable.
namespace ApplicationAcl;
use PDO;
use ApplicationDatabaseConnection;
use PsrHttpMessage { RequestInterface, ResponseInterface };
use ApplicationMiddleWare { Response, TextStream };
class DbTable implements AuthenticateInterface
{
const ERROR_AUTH = 'ERROR: authentication error';
protected $conn;
protected $table;
public function __construct(Connection $conn, $tableName)
{
$this->conn = $conn;
$this->table = $tableName;
}
public function login(RequestInterface $request) :
ResponseInterface
{
$code = 401;
$info = FALSE;
$body = new TextStream(self::ERROR_AUTH);
$params = json_decode($request->getBody()->getContents());
$response = new Response();
$username = $params->username ?? FALSE;
if ($username) {
$sql = 'SELECT * FROM ' . $this->table
. ' WHERE email = ?';
$stmt = $this->conn->pdo->prepare($sql);
$stmt->execute([$username]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
if (password_verify($params->password,
$row['password'])) {
unset($row['password']);
$body =
new TextStream(json_encode($row));
$response->withBody($body);
$code = 202;
$info = $row;
}
}
}
return $response->withBody($body)->withStatus($code);
}
}
Best practice
Never store passwords in clear text. When you need to do a password match, use password_verify(), which negates the need to reproduce the password hash.
namespace ApplicationAcl;
use ApplicationMiddleWare { Response, TextStream };
use PsrHttpMessage { RequestInterface, ResponseInterface };
class Authenticate
{
const ERROR_AUTH = 'ERROR: invalid token';
const DEFAULT_KEY = 'auth';
protected $adapter;
protected $token;
public function __construct(
AuthenticateInterface $adapter, $key)
{
$this->key = $key;
$this->adapter = $adapter;
}
public function getToken()
{
$this->token = bin2hex(random_bytes(16));
$_SESSION['token'] = $this->token;
return $this->token;
}
public function matchToken($token)
{
$sessToken = $_SESSION['token'] ?? date('Ymd');
return ($token == $sessToken);
}
public function getLoginForm($action = NULL)
{
$action = ($action) ? 'action="' . $action . '" ' : '';
$output = '<form method="post" ' . $action . '>';
$output .= '<table><tr><th>Username</th><td>';
$output .= '<input type="text" name="username" /></td>';
$output .= '</tr><tr><th>Password</th><td>';
$output .= '<input type="password" name="password" />';
$output .= '</td></tr><tr><th> </th>';
$output .= '<td><input type="submit" /></td>';
$output .= '</tr></table>';
$output .= '<input type="hidden" name="token" value="';
$output .= $this->getToken() . '" />';
$output .= '</form>';
return $output;
}
public function login(
RequestInterface $request) : ResponseInterface
{
$params = json_decode($request->getBody()->getContents());
$token = $params->token ?? FALSE;
if (!($token && $this->matchToken($token))) {
$code = 400;
$body = new TextStream(self::ERROR_AUTH);
$response = new Response($code, $body);
} else {
$response = $this->adapter->login($request);
}
if ($response->getStatusCode() >= 200
&& $response->getStatusCode() < 300) {
$_SESSION[$this->key] =
json_decode($response->getBody()->getContents());
} else {
$_SESSION[$this->key] = NULL;
}
return $response;
}
}
Go ahead and define the classes presented in this recipe, summarized in the following table:
Class
|
Discussed in these steps
|
ApplicationAclAuthenticateInterface
|
1
|
ApplicationAclDbTable
|
2 - 3
|
ApplicationAclAuthenticate
|
4 - 6
|
You can then define a chap_09_middleware_authenticate.php calling program that sets up autoloading and uses the appropriate classes:
<?php
session_start();
define('DB_CONFIG_FILE', __DIR__ . '/../config/db.config.php');
define('DB_TABLE', 'customer_09');
define('SESSION_KEY', 'auth');
require __DIR__ . '/../Application/Autoload/Loader.php';
ApplicationAutoloadLoader::init(__DIR__ . '/..');
use ApplicationDatabaseConnection;
use ApplicationAcl { DbTable, Authenticate };
use ApplicationMiddleWare { ServerRequest, Request, Constants, TextStream };
You are now in a position to set up the authentication adapter and core class:
$conn = new Connection(include DB_CONFIG_FILE);
$dbAuth = new DbTable($conn, DB_TABLE);
$auth = new Authenticate($dbAuth, SESSION_KEY);
Be sure to initialize the incoming request, and set up the request to be made to the authentication class:
$incoming = new ServerRequest();
$incoming->initialize();
$outbound = new Request();
Check the incoming class method to see if it is POST. If so, pass a request to the authentication class:
if ($incoming->getMethod() == Constants::METHOD_POST) {
$body = new TextStream(json_encode(
$incoming->getParsedBody()));
$response = $auth->login($outbound->withBody($body));
}
$action = $incoming->getServerParams()['PHP_SELF'];
?>
The display logic looks like this:
<?= $auth->getLoginForm($action) ?>
Here is the output from an invalid authentication attempt. Notice the 401 status code on the right. In this illustration, you could add a var_dump() of the response object.
Here is a successful authentication:
One of the primary reasons for the development of PSR-7 (and middleware) was a growing need to make calls between frameworks. It is of interest to note that the main documentation for PSR-7 is hosted by PHP Framework Interop Group (PHP-FIG).
namespace ApplicationMiddleWareSession;
use InvalidArgumentException;
use PsrHttpMessage {
ServerRequestInterface, ResponseInterface };
use ApplicationMiddleWare { Constants, Response, TextStream };
class Validator
{
const KEY_TEXT = 'text';
const KEY_SESSION = 'thumbprint';
const KEY_STATUS_CODE = 'code';
const KEY_STATUS_REASON = 'reason';
const KEY_STOP_TIME = 'stop_time';
const ERROR_TIME = 'ERROR: session has exceeded stop time';
const ERROR_SESSION = 'ERROR: thumbprint does not match';
const SUCCESS_SESSION = 'SUCCESS: session validates OK';
protected $sessionKey;
protected $currentPrint;
protected $storedPrint;
protected $currentTime;
protected $storedTime;
public function __construct(
ServerRequestInterface $request, $stopTime = NULL)
{
$this->currentTime = time();
$this->storedTime = $_SESSION[self::KEY_STOP_TIME] ?? 0;
$this->currentPrint =
md5($request->getServerParams()['REMOTE_ADDR']
. $request->getServerParams()['HTTP_USER_AGENT']
. $request->getServerParams()['HTTP_ACCEPT_LANGUAGE']);
$this->storedPrint = $_SESSION[self::KEY_SESSION]
?? NULL;
if (empty($this->storedPrint)) {
$this->storedPrint = $this->currentPrint;
$_SESSION[self::KEY_SESSION] = $this->storedPrint;
if ($stopTime) {
$this->storedTime = $stopTime;
$_SESSION[self::KEY_STOP_TIME] = $stopTime;
}
}
}
public function __invoke(
ServerRequestInterface $request, Response $response)
{
$code = 401; // unauthorized
if ($this->currentPrint != $this->storedPrint) {
$text[self::KEY_TEXT] = self::ERROR_SESSION;
$text[self::KEY_STATUS_REASON] =
Constants::STATUS_CODES[401];
} elseif ($this->storedTime) {
if ($this->currentTime > $this->storedTime) {
$text[self::KEY_TEXT] = self::ERROR_TIME;
$text[self::KEY_STATUS_REASON] =
Constants::STATUS_CODES[401];
} else {
$code = 200; // success
}
}
if ($code == 200) {
$text[self::KEY_TEXT] = self::SUCCESS_SESSION;
$text[self::KEY_STATUS_REASON] =
Constants::STATUS_CODES[200];
}
$text[self::KEY_STATUS_CODE] = $code;
$body = new TextStream(json_encode($text));
return $response->withStatus($code)->withBody($body);
}
<?php
use ZendExpressiveContainerApplicationFactory;
use ZendExpressiveHelper;
return [
'dependencies' => [
'factories' => [
HelperServerUrlMiddleware::class =>
HelperServerUrlMiddlewareFactory::class,
HelperUrlHelperMiddleware::class =>
HelperUrlHelperMiddlewareFactory::class,
// insert your own class here
],
],
'middleware_pipeline' => [
'always' => [
'middleware' => [
HelperServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
HelperUrlHelperMiddleware::class,
// insert reference to middleware here
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [
// Add error middleware here.
],
'error' => true,
'priority' => -10000,
],
],
];
session_start(); // to support use of $_SESSION
$loader = include __DIR__ . '/libraries/vendor/autoload.php';
$loader->add('Application', __DIR__ . '/libraries/vendor');
$loader->add('Psr', __DIR__ . '/libraries/vendor');
$session = JFactory::getSession();
$request =
(new ApplicationMiddleWareServerRequest())->initialize();
$response = new ApplicationMiddleWareResponse();
$validator = new ApplicationSecuritySessionValidator(
$request, $session);
$response = $validator($request, $response);
if ($response->getStatusCode() != 200) {
// take some action
}
First, create the ApplicationMiddleWareSessionValidator test middleware class described in steps 2 - 5. Then you will need to go to getcomposer.org and follow the directions to obtain Composer. Next, build a basic Zend Expressive application, as shown next. Be sure to select No when prompted for minimal skeleton:
cd /path/to/source/for/this/chapter
php composer.phar create-project zendframework/zend-expressive-skeleton expressive
This will create a folder /path/to/source/for/this/chapter/expressive. Change to this directory. Modify public/index.php as follows:
<?php
if (php_sapi_name() === 'cli-server'
&& is_file(__DIR__ . parse_url(
$_SERVER['REQUEST_URI'], PHP_URL_PATH))
) {
return false;
}
chdir(dirname(__DIR__));
session_start();
$_SESSION['time'] = time();
$appDir = realpath(__DIR__ . '/../../..');
$loader = require 'vendor/autoload.php';
$loader->add('Application', $appDir);
$container = require 'config/container.php';
$app = $container->get(ZendExpressiveApplication::class);
$app->run();
You will then need to create a wrapper class that invokes our session validator middleware. Create a SessionValidateAction.php file that needs to go in the /path/to/source/for/this/chapter/expressive/src/App/Action folder. For the purposes of this illustration, set the stop time parameter to a short duration. In this case, time() + 10 gives you 10 seconds:
namespace AppAction;
use ApplicationMiddleWareSessionValidator;
use ZendDiactoros { Request, Response };
use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
class SessionValidateAction
{
public function __invoke(ServerRequestInterface $request,
ResponseInterface $response, callable $next = null)
{
$inbound = new Response();
$validator = new Validator($request, time()+10);
$inbound = $validator($request, $response);
if ($inbound->getStatusCode() != 200) {
session_destroy();
setcookie('PHPSESSID', 0, time()-300);
$params = json_decode(
$inbound->getBody()->getContents(), TRUE);
echo '<h1>',$params[Validator::KEY_TEXT],'</h1>';
echo '<pre>',var_dump($inbound),'</pre>';
exit;
}
return $next($request,$response);
}
}
You will now need to add the new class to the middleware pipeline. Modify config/autoload/middleware-pipeline.global.php as follows. Modifications are shown in bold:
<?php
use ZendExpressiveContainerApplicationFactory;
use ZendExpressiveHelper;
return [
'dependencies' => [
'invokables' => [
AppActionSessionValidateAction::class =>
AppActionSessionValidateAction::class,
],
'factories' => [
HelperServerUrlMiddleware::class =>
HelperServerUrlMiddlewareFactory::class,
HelperUrlHelperMiddleware::class =>
HelperUrlHelperMiddlewareFactory::class,
],
],
'middleware_pipeline' => [
'always' => [
'middleware' => [
HelperServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
HelperUrlHelperMiddleware::class,
AppActionSessionValidateAction::class,
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [
// Add error middleware here.
],
'error' => true,
'priority' => -10000,
],
],
];
You might also consider modifying the home page template to show the status of $_SESSION. The file in question is /path/to/source/for/this/chapter/expressive/templates/app/home-page.phtml. Simply adding var_dump($_SESSION) should suffice.
Initially, you should see something like this:
After 10 seconds, refresh the browser. You should now see this:
Except in cases where you are trying to communicate between different versions of PHP, PSR-7 middleware will be of minimal use. Recall what the acronym stands for: PHP Standards Recommendations. Accordingly, if you need to make a request to an application written in another language, treat it as you would any other web service HTTP request.
class Application_MiddleWare_ServerRequest
extends Application_MiddleWare_Request
implements Psr_Http_Message_ServerRequestInterface
{
var $serverParams;
var $cookies;
var $queryParams;
// not all properties are shown
function initialize()
{
$params = $this->getServerParams();
$this->getCookieParams();
$this->getQueryParams();
$this->getUploadedFiles;
$this->getRequestMethod();
$this->getContentType();
$this->getParsedBody();
return $this->withRequestTarget($params['REQUEST_URI']);
}
function getServerParams()
{
if (!$this->serverParams) {
$this->serverParams = $_SERVER;
}
return $this->serverParams;
}
// not all getXXX() methods are shown to conserve space
function getRequestMethod()
{
$params = $this->getServerParams();
$method = isset($params['REQUEST_METHOD'])
? $params['REQUEST_METHOD'] : '';
$this->method = strtolower($method);
return $this->method;
}
function getParsedBody()
{
if (!$this->parsedBody) {
if (($this->getContentType() ==
Constants::CONTENT_TYPE_FORM_ENCODED
|| $this->getContentType() ==
Constants::CONTENT_TYPE_MULTI_FORM)
&& $this->getRequestMethod() ==
Constants::METHOD_POST)
{
$this->parsedBody = $_POST;
} elseif ($this->getContentType() ==
Constants::CONTENT_TYPE_JSON
|| $this->getContentType() ==
Constants::CONTENT_TYPE_HAL_JSON)
{
ini_set("allow_url_fopen", true);
$this->parsedBody =
file_get_contents('php://stdin');
} elseif (!empty($_REQUEST)) {
$this->parsedBody = $_REQUEST;
} else {
ini_set("allow_url_fopen", true);
$this->parsedBody =
file_get_contents('php://stdin');
}
}
return $this->parsedBody;
}
function withParsedBody($data)
{
$this->parsedBody = $data;
return $this;
}
function withoutAttribute($name)
{
if (isset($this->attributes[$name])) {
unset($this->attributes[$name]);
}
return $this;
}
}
$request = new Request(
TARGET_WEBSITE_URL,
Constants::METHOD_POST,
new TextStream($contents),
[Constants::HEADER_CONTENT_TYPE =>
Constants::CONTENT_TYPE_FORM_ENCODED,
Constants::HEADER_CONTENT_LENGTH => $body->getSize()]
);
$data = http_build_query(['data' =>
$request->getBody()->getContents()]);
$defaults = array(
CURLOPT_URL => $request->getUri()->getUriString(),
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $data,
);
$ch = curl_init();
curl_setopt_array($ch, $defaults);
$response = curl_exec($ch);
curl_close($ch);
In this article, we learned about providing authentication to a system, to make calls between frameworks, and to make a request to an application written in another language.
Further resources on this subject: