16 min read

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.)

  • Authenticating with middleware
  • Making inter-framework system calls
  • Using middleware to cross languages

Introduction

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.

Authenticating with middleware

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.

How to do it…

  1. We begin by defining a ApplicationAclAuthenticateInterface class. We use this interface to support the Adapter software design pattern, making our Authenticate class more generically useful by allowing a variety of adapters, each of which can draw authentication from a different source (for example, from a file, using OAuth2, and so on). Note the use of the PHP 7 ability to define the return value data type:
    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.

  2. Next, we define the adapter that implements the login() method required by the interface. We make sure to use the appropriate classes, and define fitting constants and properties. The constructor makes use of ApplicationDatabaseConnection:
    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;
            }
  3. The core login() method extracts the username and password from the request object. We then do a straightforward database lookup. If there is a match, we store user information in the response body, JSON-encoded:
          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.

  4. The Authenticate class is a wrapper for an adapter class that implements AuthenticationInterface. Accordingly, the constructor takes an adapter class as an argument, as well as a string that serves as the key in which authentication information is stored in $_SESSION:
    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;
            }
  5. In addition, we provide a login form with a security token, which helps prevent Cross Site Request Forgery (CSRF) attacks:
    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>&nbsp;</th>';
            $output .= '<td><input type="submit" /></td>';
            $output .= '</tr></table>';
            $output .= '<input type="hidden" name="token" value="';
            $output .= $this->getToken() . '" />';
            $output .= '</form>';
            return $output;
          }
  6. Finally, the login() method in this class checks whether the token is valid. If not, a 400 response is returned. Otherwise, the login() method of the adapter is called:
          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;
        }
    
    }

How it works…

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.

PHP 7 Programming Cookbook

Here is a successful authentication:

PHP 7 Programming Cookbook

Making inter-framework system calls

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).

How to do it…

  1. The primary mechanism used in middleware inter-framework calls is to create a driver program that executes framework calls in succession, maintaining a common request and response object. The request and response objects are expected to represent PsrHttpMessageServerRequestInterface and PsrHttpMessageResponseInterface respectively.
  2. For the purposes of this illustration, we define a middleware session validator. The constants and properties reflect the session thumbprint, which is a term we use to incorporate factors such as the website visitor’s IP address, browser, and language settings:
    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;
  3. The constructor takes a ServerRequestInterface instance and the session as arguments. If the session is an array (such as $_SESSION), we wrap it in a class. The reason why we do this is in case we are passed a session object, such as JSession used in Joomla. We then create the thumbprint using the factors previously mentioned factors. If the stored thumbprint is not available, we assume this is the first time, and store the current print as well as stop time, if this parameter is set. We used md5() because it’s a fast hash, and is not exposed externally and is therefore useful to this application:
    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;
                }
            }
          }
  4. It’s not required to define __invoke(), but this magic method is quite convenient for standalone middleware classes. As is the convention, we accept ServerRequestInterface and ResponseInterface instances as arguments. In this method we simply check to see if the current thumbprint matches the one stored. The first time, of course, they will match. But on subsequent requests, the chances are an attacker intent on session hijacking will be caught out. In addition, if the session time exceeds the stop time (if set), likewise, a 401 code will be sent:
          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);
          }
  5. We can now put our new middleware class to use. The main problems with inter-framework calls, at least at this point, are summarized here. Accordingly, how we implement middleware depends heavily on the last point:
    • Not all PHP frameworks are PSR-7-compliant
    • Existing PSR-7 implementations are not complete
    • All frameworks want to be the “boss”
  6. As an example, have a look at the configuration files for Zend Expressive, which is a self-proclaimed PSR7 Middleware Microframework. Here is the file, middleware-pipeline.global.php, which is located in the config/autoload folder in a standard Expressive application. The dependencies key is used to identify middleware wrapper classes that will be activated in the pipeline:
          <?php
          use ZendExpressiveContainerApplicationFactory;
          use ZendExpressiveHelper;
          return [  
            'dependencies' => [
                'factories' => [
                HelperServerUrlMiddleware::class => 
                HelperServerUrlMiddlewareFactory::class,
                HelperUrlHelperMiddleware::class => 
                HelperUrlHelperMiddlewareFactory::class,
                    // insert your own class here
                ],
            ],
  7. Under the middleware_pipline key you can identify classes that will be executed before or after the routing process occurs. Optional parameters include path, error, and priority:
    '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,
              ],
            ],
          ];
  8. Another technique is to modify the source code of an existing framework module, and make a request to a PSR-7-compliant middleware application. Here is an example modifying a Joomla! installation to include a middleware session validator.
  9. Next, add this code the end of the index.php file in the /path/to/joomla folder. Since Joomla! uses Composer, we can leverage the Composer autoloader:
    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');
  10. We can then create an instance of our middleware session validator, and make a validation request just before $app = JFactory::getApplication(‘site’);:
    $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
          }

How it works…

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:

PHP 7 Programming Cookbook

After 10 seconds, refresh the browser. You should now see this:

PHP 7 Programming Cookbook

Using middleware to cross languages

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.

How to do it…

  1. In the case of PHP 4, you actually have a chance in that there was limited support for object-oriented programming. There is not enough space to cover all changes, but we present a potential PHP 4 version of ApplicationMiddleWareServerRequest. The first thing to note is that there are no namespaces! Accordingly, we use a classname with underscores, _, in place of namespace separators:
            class Application_MiddleWare_ServerRequest
            extends Application_MiddleWare_Request
            implements Psr_Http_Message_ServerRequestInterface
            {
  2. All properties are identified in PHP 4 using the key word var:
    var $serverParams;
          var $cookies;
          var $queryParams;
          // not all properties are shown
  3. The initialize() method is almost the same, except that syntax such as $this->getServerParams()[‘REQUEST_URI’] was not allowed in PHP 4. Accordingly, we need to split this out into a separate variable:
    function initialize()
          {
            $params = $this->getServerParams();
            $this->getCookieParams();
            $this->getQueryParams();
            $this->getUploadedFiles;
            $this->getRequestMethod();
            $this->getContentType();
            $this->getParsedBody();
            return $this->withRequestTarget($params['REQUEST_URI']);
          }
  4. All of the $_XXX super-globals were present in later versions of PHP 4:
    function getServerParams()
          {
            if (!$this->serverParams) {
                $this->serverParams = $_SERVER;
            }
            return $this->serverParams;
        }
        // not all getXXX() methods are shown to conserve space
  5. The null coalesce” operator was only introduced in PHP 7. We need to use isset(XXX) ? XXX : ”; instead:
    function getRequestMethod()
          {
            $params = $this->getServerParams();
            $method = isset($params['REQUEST_METHOD']) 
    ? $params['REQUEST_METHOD'] : '';
            $this->method = strtolower($method);
            return $this->method;
          }
  6. The JSON extension was not introduced until PHP 5. Accordingly, we need to be satisfied with raw input. We could also possibly use serialize() or unserialize() in place of json_encode() and json_decode():
    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;
          }
  7. The withXXX() methods work pretty much the same in PHP 4:
    function withParsedBody($data)
          {
            $this->parsedBody = $data;
            return $this;
          }
  8. Likewise, the withoutXXX() methods work the same as well:
    function withoutAttribute($name)
          {
            if (isset($this->attributes[$name])) {
                unset($this->attributes[$name]);
            }
            return $this;
            }
    
          }
  9. For websites using other languages, we could use the PSR-7 classes to formulate requests and responses, but would then need to use an HTTP client to communicate with the other website. Here is the example:
    $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);

Summary

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.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here