CakePHP 4.x Nutzer Authentifizierungs Tutorial

6 October 2020

In diesem Blogartikel wird beschrieben, wie Ihr eine simple Nutzer Authentifizierung für CakePHP mittels des offiziell unterstützen Authentication Plugins aufbaut.

Authentifizierung in Web Anwendungen ist der Nachweis der Identität eines Nutzers, d.h. ist der Nutzer der, der er vorgibt zu sein. Authentifizierung findet meist über Nutzername/Passwort, Sessions/Cookies oder JWT/OAuth statt. Ob ein bestimmter Nutzer auf eine bestimmte Resource (z.B. Website) zugreifen darf ist hingegen Teil der Authorisierung und wird in einem weiteren Artikel beschrieben.

Grundlage dieses Tutorials ist das CakePHP Quickstart Tutorial.

Installation des Plugins

Installiert das Plugin mit Composer im root Verzeichnis eurer App:

composer require cakephp/authentication:^2.0

Das Plugin in eure Application laden:

// src/Application.php
public function bootstrap(): void
{
    // Call parent to load bootstrap from files.
    parent::bootstrap();

    ...

    // Load more plugins here
    $this->addPlugin('Authentication');
}

Middleware

Das Authentication Plugin wird mittels einer PSR-7 Middleware integriert; diese prüft bei jedem Request die Identität des Nutzers mittels eines Authentifikators (z.B. Form Authenticator mit Nutzername/Passwort). Um die Middleware nutzen zu können muss unsere Application das AuthenticationProviderInterface implementieren und die Middleware in der middleware() Methode hinzugefügt werden:

// src/Application.php
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Core\Configure;
use Cake\Core\Exception\MissingPluginException;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\Middleware\BodyParserMiddleware;
use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\MiddlewareQueue;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Psr\Http\Message\ServerRequestInterface;

class Application extends BaseApplication implements AuthenticationServiceProviderInterface
{
    /**
     * Load all the application configuration and bootstrap logic.
     *
     * @return void
     */
    public function bootstrap(): void
    {
        // Call parent to load bootstrap from files.
        parent::bootstrap();

        ...

        // Load more plugins here
        $this->addPlugin('Authentication');
    }

    /**
     * Setup the middleware queue your application will use.
     *
     * @param \Cake\Http\MiddlewareQueue $middlewareQueue The middleware queue to setup.
     * @return \Cake\Http\MiddlewareQueue The updated middleware queue.
     */
    public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
    {
        $middlewareQueue
            // load other Middleware

            ...

            // Authentication Middleware
            ->add(new AuthenticationMiddleware($this));


        return $middlewareQueue;
    }

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        // TODO: Implement getAuthenticationService() method.
    }
}

Mehr zur Authentication Middleware und zu CakePHP Middleware.

Als nächstes implementieren wir die getAuthenticationService() Methode:

// src/Application.php

use Authentication\AuthenticationService;
use Authentication\Identifier\IdentifierInterface;
...

/**
 * Returns a service provider instance.
 *
 * @param \Psr\Http\Message\ServerRequestInterface $request Request
 * @return \Authentication\AuthenticationServiceInterface
 */
 public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
 {
     // Create AuthenticationService
     $service = new AuthenticationService();

     // Define where users should be redirected to when they are not authenticated
     $service->setConfig([
         'unauthenticatedRedirect' => Router::url(['controller' => 'Users', 'action' => 'login']),
         'queryParam' => 'redirect',
     ]);

     // Fields in your db to match against
     $fields = [
         IdentifierInterface::CREDENTIAL_USERNAME => 'email',
         IdentifierInterface::CREDENTIAL_PASSWORD => 'password'
     ];

     // Load the authenticators. Session should be first.
     $service->loadAuthenticator('Authentication.Session');
     $service->loadAuthenticator('Authentication.Form', [
         'fields' => $fields,
         'loginUrl' => Router::url(['controller' => 'Users', 'action' => 'login'])
     ]);

     // Load identifiers
     $service->loadIdentifier('Authentication.Password', compact('fields'));

     return $service;
}

AuthenticationService erstellen

Hierbei wird der AuthenticationService erstellt und konfiguriert. Die Option unauthenticatedRedirect gibt an, wohin nicht authentifizierte Nutzer weitergeleitet werden sollen. Weitere Konfigurationsmöglichkeiten unter Configuration.

Authenticator hinzufügen

Als nächstes fügen wir mit loadAuthenticator() Authenticator hinzu. In diesem Beispiel wird formularbasierte Authentifizierung (Form) und PHP Session genutzt. Session sollte immer zuerst geladen werden. Die Formularfelder werden in der Option fields konfiguriert.

Identifier hinzufügen

Abschließdend wird mit loadIdentifier() noch der Identifier geladen. Falls euer Datenmodel anders aussehen sollte, kann mit der Option resolver hier ein anderes gesetzt werden. Die Option passwordHasher kann genutzt werden, um ältere Password Hashes durch neue zu ersetzen. Mehr dazu unter https://book.cakephp.org/authentication/2/en/identifiers.html#password

Controller Component hinzufügen

In unserem AppController laden wir noch die Authentication Component:

// src/Controller/AppController.php

public function initialize(): void
{
    parent::initialize();

    $this->loadComponent('RequestHandler');
    $this->loadComponent('Flash');
    $this->loadComponent('Authentication.Authentication');
}

User Login Action implementieren

Damit sich der Nutzer auch anmelden kann muss der Zugriff auf die login() Action möglich sein. Dazu wird in der beforeFilter() des UsersControllerder Zugriff mit allowUnauthenticated erlaubt. Die Login Action nutzt die AuthenticationComponent und erhält das Ergebnis des Anmeldeversuchs. Falls der Anmeldeversuch erfolgreich war, könnt ihr den Nutzer auf eine bestimmte Seite weiterleiten.

// src/Controller/UsersController.php

public function beforeFilter(EventInterface $event)
{
    $this->Authentication->allowUnauthenticated(['login']);

    parent::beforeFilter($event);
}

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

    // If the user is logged in send them away.
    if ($result->isValid()) {
        $target = $this->Authentication->getLoginRedirect() ?? '/';

        return $this->redirect($target);
    }

    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error('Invalid username or password');
    }
}

Als nächstes brauchen wir ein Anmeldeformular für unsere login() Action. Dazu nutzen wir den FormHelper:

// in templates/Users/login.php

<?= $this->Form->create() ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button(__('Login')) ?>
<?= $this->Form->end() ?>

User Registration Action implementieren

Aktuell gibt es allerdings keinen Nutzer in unserer Datenbank mit einem entsprechenden Passwort Hash. Dazu ändern wir die setter Methode für unser Passwort in der User Entity so ab, dass sie das Passwort als Hash speichert:

// in src/Model/Entity/User.php

protected function _setPassword(string $plainPassword)
{
    $hasher = new DefaultPasswordHasher();

    return $hasher->hash($plainPassword);
}

Um neue Nutzer zu registrieren erstellen wir als nächstes eine register() Action. In dieser wird eine neue Entity erstellt und das dabei eingegeben Passwort wird als Hash in der Datenbank gespeichert.

// in src/Controller/UsersController.php

public function register()
{
    $user = $this->Users->newEmptyEntity();

    if ($this->request->is('post')) {
        $user = $this->Users->patchEntity($user, $this->request->getData());

        if ($this->Users->save($user)) {
            $this->Flash->success(__('Your registration was successful.'));

            return $this->redirect('/');
        }

        $this->Flash->error(__('Your registration failed.'));
    }
}

Für das entsprechende Formular kann einfach der Inhalt der templates/Users/login.php in das Template der register Action nach templates/Users/register.php kopiert werden.

Über die URL entsprechende URL (in meinem Fall http://localhost/users/register) kann nun ein neuer Nutzer angelegt werden. Unter http://localhost/users/ solltet ihr diesen Nutzer mit dem gehashten Passwort sehen.

Den kompletten Quellcode findet ihr auf Github.