RFC: authentication module for Expressive and PSR-7 apps

Update: I created a diagram to show the usage of RFC authentication and RFC authorization.

RFC: authentication module for Expressive and PSR-7 apps

Following the RFC for Authorization Middleware, I now propose an authentication module for middleware applications.

Goal

An authentication middleware checks user credentials provided in the HTTP request and blocks the pipeline execution on authentication failure.

The goal of this RFC is to build a general authentication module for Expressive and PSR-7 applications. The idea is to authenticate user credentials based on username and password or using a token (e.g. using OAuth2).

Requirements

The authentication middleware should be able to check against different authentication mechanisms using specific adapters (e.g. HTTP Basic/Digest Authentication, PHP Session, OAuth2, etc.).

The users should be managed using adapters, to be able to retrieve credential via different data storage mechanisms (e.g. htpasswd file, PDO-backed database, etc.).

Implementation

I proposed an implementation of the authentication middleware using the following adapter interface:

namespace Zend\Expressive\Authentication;

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;

interface AuthenticationInterface
{
    /**
     * Authenticate the PSR-7 request and return a valid user
     * or null if not authenticated.
     */
    public function authenticate(ServerRequestInterface $request) : ?UserInterface;

    /**
     * Generate the unauthorized response
     */
    public function unauthorizedResponse(ServerRequestInterface $request) : ResponseInterface;
}

This interface defines 2 methods authenticate(), and unauthorizedResponse(). The first is used to authenticate a PSR-7 request, returning a UserInterface object or null if not authenticated. The second function is used to generate the unauthorized response.

For instance, using Basic Access Authentication, the authentication is provided using an Authorization header as follows:

Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l

In situations where the HTTP request does not contain this header, authentication must respond with a 401 Unauthorized status with the following header:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Basic realm="User Visible Realm"

Where the realm is a string that defines the protection space, in case of multiple authentication systems on the same server resource.

In the case of a valid credential, the authenticate() method returns a UserInterface instance. This is a simple interface described as follows:

namespace Zend\Expressive\Authentication;

interface UserInterface
{
    /**
     * Get the username
     */
    public function getUsername() : string;

    /**
     * Get the user role
     */
    public function getUserRole() : string;
}

This interface defines 2 methods, one each for retrieving the username and the user role. This information can be consumed by Authorization middleware to check for authorized users to perform specific actions (e.g. see the zend-expressive-authorization proposal).

In order to use different user repositories for credentials (e.g., to perform lookups using different storage backends), I propose the following interface:

namespace Zend\Expressive\Authentication;

interface UserRepositoryInterface
{
    /**
     * Authenticate the credential (username) using a password
     * or using only the credential string (e.g. token based credential)
     * It returns the authenticated user or null.
     *
     * @param string $credential Can be also a token
     * @param string $password
     * @return UserInterface|null
     */
    public function authenticate(string $credential, string $password = null) : ?UserInterface;
}

This interface contains only one function, authenticate. This function can be used to check if the username (credential) and password are valid or not. The password argument is optional, in case we need to authenticate against a single token string, specified by the credential parameter.

If the user is authenticated it will return a UserInterface object or null if not authenticated.

Using these level of abstractions we should be able to manage all authentication mechanisms using different user repositories.

Configuration

To configure the proposed authentication middleware we can use alias services. We can use Zend\Expressive\Authentication\AuthenticationInterface::class name for the authentication adapter and Zend\Expressive\Authentication\UserRepositoryInterface::class for the user register adapter.

The default configuration provided by a ConfigProvider class for the proposed module might look like the following:

namespace Zend\Expressive\Authentication;

class ConfigProvider
{
    /**
     * Return the configuration array.
     */
    public function __invoke() : array
    {
        return [
            'dependencies'  => $this->getDependencies(),
            'authentication' => include __DIR__ .  '/../config/authentication.php',
        ];
    }

    /**
     * Returns the container dependencies
     */
    public function getDependencies() : array
    {
        return [
            'aliases' => [
                // Change the alias value for Authentication adapter and
                // UserRepository adapter
                AuthenticationInterface::class => Adapter\BasicAccess::class,
                UserRepositoryInterface::class => UserRepository\Htpasswd::class,
            ],
            'factories' => [
                AuthenticationMiddleware::class   => AuthenticationMiddlewareFactory::class,
                Adapter\BasicAccess::class        => Adapter\BasicAccessFactory::class,
                UserRepository\Htpasswd::class    => UserRepository\HtpasswdFactory::class,
		UserRepository\PdoDatabase::class => UserRepository\PdoDatabaseFactory::class
            ],
        ];
    }
}

The config/authentication.php file contains specific configuration used by the adapters.

I have prototyped an implementation of the authentication middleware in ezimuel/zend-expressive-authentication. Currently, I only implement HTTP Basic Authentication using 2 user repositories: htpasswd file or PDO database.

2 Likes

Hi @enrico,

I would like to ask why user can have only one role and how do we handle the situation, when user has multiple roles?

This does seem a bit odd to me as well. Frequently in the applications I work with, a user may have none, one, or many scopes / roles. An iterable scopeInterface or roleInterface would seem to make sense in this case to me. Ideally, this would be composable into a Zend\Acl or Zend\Rbac implementation.That way the downstream tidbits can do their specific authorization checks at an action level … or even at a per-entity level when appropriate. Specific permission checks can then also be flexible.

@jackdpeterson, @pdrosos you right, we can extend the implementation supporting multiple roles. I did the first POC with only one role. We need to modify zend-expressive-authentication and zend-expressive-authorization to support it. Any proposal?

@enrico

Could Zend\Expressive\Authentication\UserInterface have a method getUserRoles(): array; like ZfcRbac/Identity/IdentityInterface.php in the ZfcRbac module? This way user can have 0 or more roles.

Then zend-expressive-authorization should receive an array with roles as Request argument and pass them to the corresponding adapter - ACL or Rbac. The adapters logic should be changed too, so they do authorization with multiple roles, instead of a single role.

I just sent a PR adding the support of multiple users: https://github.com/zendframework/zend-expressive-authentication/pull/4

2 Likes

I’ll think on it and get back to you. If I have some free time next week, I’ll look into coming up with a proposed implementation.