Authenticators

Authenticators handle converting request data into an authentication operations. They leverage Identifiers to find a known Identity Objects.

Session

This authenticator will check the session if it contains user data or credentials. When using any stateful authenticators like Form listed below, be sure to load Session authenticator first so that once logged in user data is fetched from session itself on subsequent requests.

Configuration options:

  • sessionKey: The session key for the user data, default is Auth

  • identify: Set this key with a value of bool true to enable checking the session credentials against the identifiers. When true, the configured Identifiers are used to identify the user using data stored in the session on each request. Default value is false.

  • fields: Allows you to map the username field to the unique identifier in your user storage. Defaults to username. This option is used when the identify option is set to true.

Form

Looks up the data in the request body, usually when a form submit happens via POST / PUT.

Configuration options:

  • loginUrl: The login URL, string or array of URLs. Default is null and all pages will be checked.

  • fields: Array that maps username and password to the specified POST data fields.

  • urlChecker: The URL checker class or object. Default is DefaultUrlChecker.

  • useRegex: Whether or not to use regular expressions for URL matching. Default is false.

  • checkFullUrl: Whether or not to check full URL including the query string. Useful when a login form is on a different subdomain. Default is false. This option does not work well when preserving unauthenticated redirects in the query string.

If you are building an API and want to accept credentials via JSON requests make sure you have the BodyParserMiddleware applied before the AuthenticationMiddleware.

Warning

If you use the array syntax for the URL, the URL will be generated by the CakePHP router. The result might differ from what you actually have in the request URI depending on your route handling. So consider this to be case sensitive!

Token

The token authenticator can authenticate a request based on a token that comes along with the request in the headers or in the request parameters.

Configuration options:

  • queryParam: Name of the query parameter. Configure it if you want to get the token from the query parameters.

  • header: Name of the header. Configure it if you want to get the token from the header.

  • tokenPrefix: The optional token prefix.

An example of getting a token from a header, or query string would be:

$service->loadAuthenticator('Authentication.Token', [
    'queryParam' => 'token',
    'header' => 'Authorization',
    'tokenPrefix' => 'Token'
]);

The above would read the token GET parameter or the Authorization header as long as the token was preceded by Token and a space.

The token will always be passed to the configured identifier as follows:

[
    'token' => '{token-value}',
]

JWT

The JWT authenticator gets the JWT token from the header or query param and either returns the payload directly or passes it to the identifiers to verify them against another datasource for example.

  • header: The header line to check for the token. The default is Authorization.

  • queryParam: The query param to check for the token. The default is token.

  • tokenPrefix: The token prefix. Default is bearer.

  • algorithm: The hashing algorithm for Firebase JWT. Default is 'HS256'.

  • returnPayload: To return or not return the token payload directly without going through the identifiers. Default is true.

  • secretKey: Default is null but you’re required to pass a secret key if you’re not in the context of a CakePHP application that provides it through Security::salt().

  • jwks: Default is null. Associative array with a 'keys' key. If provided will be used instead of the secret key.

You need to add the lib firebase/php-jwt v6.2 or above to your app to use the JwtAuthenticator.

By default the JwtAuthenticator uses HS256 symmetric key algorithm and uses the value of Cake\Utility\Security::salt() as encryption key. For enhanced security one can instead use the RS256 asymmetric key algorithm. You can generate the required keys for that as follows:

# generate private key
openssl genrsa -out config/jwt.key 1024
# generate public key
openssl rsa -in config/jwt.key -outform PEM -pubout -out config/jwt.pem

The jwt.key file is the private key and should be kept safe. The jwt.pem file is the public key. This file should be used when you need to verify tokens created by external applications, eg: mobile apps.

The following example allows you to identify the user based on the sub (subject) of the token by using JwtSubject identifier, and configures the Authenticator to use public key for token verification.

Add the following to your Application class:

public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $service = new AuthenticationService();
    // ...
    $service->loadIdentifier('Authentication.JwtSubject');
    $service->loadAuthenticator('Authentication.Jwt', [
        'secretKey' => file_get_contents(CONFIG . '/jwt.pem'),
        'algorithm' => 'RS256',
        'returnPayload' => false
    ]);
}

In your UsersController:

use Firebase\JWT\JWT;

public function login()
{
    $result = $this->Authentication->getResult();
    if ($result->isValid()) {
        $privateKey = file_get_contents(CONFIG . '/jwt.key');
        $user = $result->getData();
        $payload = [
            'iss' => 'myapp',
            'sub' => $user->id,
            'exp' => time() + 60,
        ];
        $json = [
            'token' => JWT::encode($payload, $privateKey, 'RS256'),
        ];
    } else {
        $this->response = $this->response->withStatus(401);
        $json = [];
    }
    $this->set(compact('json'));
    $this->viewBuilder()->setOption('serialize', 'json');
}

Using a JWKS fetched from an external JWKS endpoint is supported as well:

// Application.php
public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $service = new AuthenticationService();
    // ...
    $service->loadIdentifier('Authentication.JwtSubject');

    $jwksUrl = 'https://appleid.apple.com/auth/keys';

    // Set of keys. The "keys" key is required. Additionally keys require a "alg" key.
    // Add it manually to your JWK array if it doesn't already exist.
    $jsonWebKeySet = Cache::remember('jwks-' . md5($jwksUrl), function () use ($jwksUrl) {
        $http = new Client();
        $response = $http->get($jwksUrl);
        return $response->getJson();
    });

    $service->loadAuthenticator('Authentication.Jwt', [
        'jwks' => $jsonWebKeySet,
        'returnPayload' => false
    ]);
}

The JWKS resource will return the same set of keys most of the time. Applications should cache these resources, but they also need to be prepared to handle signing key rotations.

Warning

Applications need to pick a cache lifetime that balances performance and security. This is particularly important in situations where a private key is compromised.

Beside from sharing the public key file to external application, you can distribute it via a JWKS endpoint by configuring your app as follows:

// config/routes.php
$builder->setExtensions('json');
$builder->connect('/.well-known/:controller/*', [
    'action' => 'index',
], [
    'controller' => '(jwks)',
]); // connect /.well-known/jwks.json to JwksController

// controller/JwksController.php
public function index()
{
    $pubKey = file_get_contents(CONFIG . './jwt.pem');
    $res = openssl_pkey_get_public($pubKey);
    $detail = openssl_pkey_get_details($res);
    $key = [
        'kty' => 'RSA',
        'alg' => 'RS256',
        'use' => 'sig',
        'e' => JWT::urlsafeB64Encode($detail['rsa']['e']),
        'n' => JWT::urlsafeB64Encode($detail['rsa']['n']),
    ];
    $keys['keys'][] = $key;

    $this->viewBuilder()->setClassName('Json');
    $this->set(compact('keys'));
    $this->viewBuilder()->setOption('serialize', 'keys');
}

Refer to https://datatracker.ietf.org/doc/html/rfc7517 or https://auth0.com/docs/tokens/json-web-tokens/json-web-key-sets for more information about JWKS.

HttpBasic

See https://en.wikipedia.org/wiki/Basic_access_authentication

Note

This authenticator will halt the request when authentication credentials are missing or invalid.

Configuration options:

  • realm: Default is $_SERVER['SERVER_NAME'] override it as needed.

HttpDigest

See https://en.wikipedia.org/wiki/Digest_access_authentication

Note

This authenticator will halt the request when authentication credentials are missing or invalid.

Configuration options:

  • realm: Default is null

  • qop: Default is auth

  • nonce: Default is uniqid(''),

  • opaque: Default is null

Environment Variables

The EnvironmentAuthenticator can authenticate users based on mapped environment variables exposed by the webserver. This enables authentication via Shibboleth and similar SAML 1.1 implementations. An example configuration is:

// Configure a token identifier that maps `USER_ID` to the
// username column
$service->loadIdentifier('Authentication.Token', [
    'tokenField' => 'username',
    'dataField' => 'USER_NAME',
]);

$service->loadAuthenticator('Authentication.Environment', [
    'loginUrl' => '/sso',
    'fields' => [
        // Choose which environment variables exposed by your
        // authentication provider are used to authenticate
        // in your application.
        'USER_NAME',
    ],
]);

New in version 2.10.0: EnvironmentAuthenticator was added.

Events

There is only one event that is fired by authentication: Authentication.afterIdentify.

If you don’t know what events are and how to use them check the documentation.

The Authentication.afterIdentify event is fired by the AuthenticationComponent after an identity was successfully identified.

The event contains the following data:

  • provider: An object that implements \Authentication\Authenticator\AuthenticatorInterface

  • identity: An object that implements \ArrayAccess

  • service: An object that implements \Authentication\AuthenticationServiceInterface

The subject of the event will be the current controller instance the AuthenticationComponent is attached to.

But the event is only fired if the authenticator that was used to identify the identity is not persistent and not stateless. The reason for this is that the event would be fired every time because the session authenticator or token for example would trigger it every time for every request.

From the included authenticators only the FormAuthenticator will cause the event to be fired. After that the session authenticator will provide the identity.

URL Checkers

Some authenticators like Form or Cookie should be executed only on certain pages like /login page. This can be achieved using URL Checkers.

By default a DefaultUrlChecker is used, which uses string URLs for comparison with support for regex check.

Configuration options:

  • useRegex: Whether or not to use regular expressions for URL matching. Default is false.

  • checkFullUrl: Whether or not to check full URL. Useful when a login form is on a different subdomain. Default is false.

A custom URL checker can be implemented for example if a support for framework specific URLs is needed. In this case the Authentication\UrlChecker\UrlCheckerInterface should be implemented.

For more details about URL Checkers see this documentation page.

Getting the Successful Authenticator or Identifier

After a user has been authenticated you may want to inspect or interact with the Authenticator that successfully authenticated the user:

// In a controller action
$service = $this->request->getAttribute('authentication');

// Will be null on authentication failure, or an authenticator.
$authenticator = $service->getAuthenticationProvider();

You can also get the identifier that identified the user as well:

// In a controller action
$service = $this->request->getAttribute('authentication');

// Will be null on authentication failure, or an identifier.
$identifier = $service->getIdentificationProvider();

Using Stateless Authenticators with Stateful Authenticators

When using HttpBasic, HttpDigest with other authenticators, you should remember that these authenticators will halt the request when authentication credentials are missing or invalid. This is necessary as these authenticators must send specific challenge headers in the response:

use Authentication\AuthenticationService;

// Instantiate the service
$service = new AuthenticationService();

// Load identifiers
$service->loadIdentifier('Authentication.Password', [
    'fields' => [
        'username' => 'email',
        'password' => 'password'
    ]
]);
$service->loadIdentifier('Authentication.Token');

// Load the authenticators leaving Basic as the last one.
$service->loadAuthenticator('Authentication.Session');
$service->loadAuthenticator('Authentication.Form');
$service->loadAuthenticator('Authentication.HttpBasic');

If you want to combine HttpBasic or HttpDigest with other authenticators, be aware that these authenticators will abort the request and force a browser dialog.

Handling Unauthenticated Errors

The AuthenticationComponent will raise an exception when users are not authenticated. You can convert this exception into a redirect using the unauthenticatedRedirect when configuring the AuthenticationService.

You can also pass the current request target URI as a query parameter using the queryParam option:

// In the getAuthenticationService() method of your src/Application.php

$service = new AuthenticationService();

// Configure unauthenticated redirect
$service->setConfig([
    'unauthenticatedRedirect' => '/users/login',
    'queryParam' => 'redirect',
]);

Then in your controller’s login method you can use getLoginRedirect() to get the redirect target safely from the query string parameter:

public function login()
{
    $result = $this->Authentication->getResult();

    // Regardless of POST or GET, redirect if user is logged in
    if ($result->isValid()) {
        // Use the redirect parameter if present.
        $target = $this->Authentication->getLoginRedirect();
        if (!$target) {
            $target = ['controller' => 'Pages', 'action' => 'display', 'home'];
        }
        return $this->redirect($target);
    }
}

Having Multiple Authentication Flows

In an application that provides both an API and a web interface you may want different authentication configurations based on whether the request is an API request or not. For example, you may use JWT authentication for your API, but sessions for your web interface. To support this flow you can return different authentication services based on the URL path, or any other request attribute:

public function getAuthenticationService(
    ServerRequestInterface $request
): AuthenticationServiceInterface {
    $service = new AuthenticationService();

    // Configuration common to both the API and web goes here.

    if ($request->getParam('prefix') == 'Api') {
        // Include API specific authenticators
    } else {
        // Web UI specific authenticators.
    }

    return $service;
}