CakePHP3で遊ぶ


カスタム認証オブジェクトに挑戦してみる

CakePHP 3に標準で用意されているForm認証モジュールは、ユーザIDとパスワード以外の値を入力して認証することを考慮していません。
組織などのグループに所属するユーザを認証する際にグループIDを入力して認証するには、カスタム認証モジュールを利用することになります。

Form認証の基本

まず、標準のForm認証モジュールで認証する方法です。
基本のUsersテーブルだけで認証する場合は、次のようなコードを使用します。

Controller/AppController.php
    public function initialize()
    {
             :
        /*
         * ユーザ認証機能を登録
         */
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Form' => [
                    'fields' => [
                        'username' => 'email',
                        'password' => 'password',
                    ],
                ],
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login',
            ],
             :
        ]);
Template/Users/login.ctp
<h1><?= __('Sign In') ?></h1>
<?= $this->Form->create() ?> 
<?= $this->Form->control('email') ?> 
<?= $this->Form->control('password') ?> 
<?= $this->Form->button(__('Sign In')) ?> 
<?= $this->Form->end() ?> 

この記述でUsersテーブルにあるemailカラムとpasswordカラムを使って、認証することができるようになります。

Form認証を拡張する

次に、以下のような2つのテーブルを用いて認証することを検討してみたいと思います。

create table `groups` (
    `id` INT not null AUTO_INCREMENT
  , `code` VARCHAR(16) not null
  , `name` VARCHAR(32) not null
  , `expiration` DATETIME not null
  , `status` CHAR(1) not null
);

create unique index `groups_IX1`
  on `groups`(`code`);

create table `users` (
    `id` CHAR(36) not null
  , `group_id` INT not null
  , `code` VARCHAR(32) not null
  , `name` VARCHAR(250) not null
  , `password` VARCHAR(250) not null
  , `status` CHAR(1) not null
);

create unique index `users_IX1`
  on `users`(`group_id`,`code`);

〇〇グループに所属する□□さんを認証することを目論んでいます。
そのため、テンプレートは、

Template/Users/login.ctp
<h1><?= __('Sign In') ?></h1>
<?= $this->Form->create() ?> 
<?= $this->Form->control('group') ?> 
<?= $this->Form->control('code') ?> 
<?= $this->Form->control('password') ?> 
<?= $this->Form->button(__('Sign In')) ?> 
<?= $this->Form->end() ?> 

のようにします。
Authコンポーネントの登録は、

Controller/AppController.php
    public function initialize()
             :
        /*
         * ユーザ認証機能を登録
         */
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Myform' => [
                    'fields' => [
                        'username' => 'code',
                        'password' => 'password',
                    ],
                    'finder' => 'auth',
                    'contain' => [
                        'Groups' => [
                            'fields' => [
                                'code' => 'group',
                            ],
                            'conditions' => [
                                'Groups.status <>' => 'D',
                                'Groups.expiration >= NOW()'
                            ],
                        ]
                    ],
                ],
            ],
            'loginAction' => [
                'controller' => 'Users',
                'action' => 'login',
            ],
             :
        ]);

のようにします。
標準の認証モジュールからはcontainの設定方法を拡張しています。
拡張に伴ってcontainの設定は、

'contain' => [
    '<モデルのクラス名>' => [
        'fields' => [
            '<カラム名>' => '<フィールド名>',
            ...
        ],
        'conditions' => [
            '<検索条件>',
            ...
        ],
    ]
],

の形式で登録します。
次にこの設定に合わせ、Form認証モジュールを参考にしてカスタム認証モジュールを作成します。

Auth/MyformAuthenticate.php
<?php
namespace App\Auth;

use Cake\Http\Response;
use Cake\Http\ServerRequest;
use Cake\Auth\BaseAuthenticate;
use Cake\ORM\TableRegistry;

class MyformAuthenticate extends BaseAuthenticate
{
    protected $_defaultConfig = [
        'fields' => [
            'username' => 'username',
            'password' => 'password',
        ],
        'userModel' => 'Users',
        'scope' => [],
        'finder' => 'all',
        'contain' => null,
        'passwordHasher' => 'Default',
    ];

    protected function _checkFields(ServerRequest $request, array $fields)
    {
        foreach ([$fields['username'], $fields['password']] as $field) {
            $value = $request->getData($field);
            if (empty($value) || !is_string($value)) {
                return false;
            }
        }

        return true;
    }

    protected function _checkContainFields(ServerRequest $request, array $contains)
    {
        foreach ($contains as $contain) {
            $fields = $contain['fields'];
            foreach ($fields as $column => $field) {
                $value = $request->getData($field);
                if (empty($value) || !is_string($value)) {
                    return false;
                }
            }
        }

        return true;
    }

    protected function _query($username, array $params = null)
    {
        $config = $this->_config;
        $table = TableRegistry::get($config['userModel']);

        $options = [
            'conditions' => [$table->aliasField($config['fields']['username']) => $username]
        ];

        if (!empty($config['scope'])) {
            $options['conditions'] = array_merge($options['conditions'], $config['scope']);
        }
        if (!empty($config['contain'])) {
            if (is_array($config['contain'])) {
                foreach ($config['contain'] as $key => $val) {

                    $conditions = [];
                    foreach ($val['fields'] as $column => $name) {
                        $conditions += [$key.'.'.$column => $params[$key][$column]];
                    }
                    if (!empty($val['conditions'])) {
                        $conditions = array_merge($val['conditions'], $conditions);
                    }
            
                    $options['contain'][] = $key;
                    $options['conditions'] = array_merge($options['conditions'], $conditions);
                }
            }
            else {
                $options['contain'] = $config['contain'];
            }
        }

        $finder = $config['finder'];
        if (is_array($finder)) {
            $options += current($finder);
            $finder = key($finder);
        }

        if (!isset($options['username'])) {
            $options['username'] = $username;
        }

        return $table->find($finder, $options);
    }

    protected function _findUser($username, $password = null, array $params = null)
    {
        $query = $this->_query($username, $params);
        $result = $query->first();

        if (empty($result)) {
            $hasher = $this->passwordHasher();
            $hasher->hash((string)$password);

            return false;
        }

        $passwordField = $this->_config['fields']['password'];
        if ($password !== null) {
            $hasher = $this->passwordHasher();
            $hashedPassword = $result->get($passwordField);
            if (!$hasher->check($password, $hashedPassword)) {
                return false;
            }

            $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);
            $result->unsetProperty($passwordField);
        }
        $hidden = $result->getHidden();
        if ($password === null && in_array($passwordField, $hidden)) {
            $key = array_search($passwordField, $hidden);
            unset($hidden[$key]);
            $result->setHidden($hidden);
        }

        return $result->toArray();
    }

    public function authenticate(ServerRequest $request, Response $response)
    {
        $fields = $this->_config['fields'];
        if (!$this->_checkFields($request, $fields)) {
            return false;
        }
        $contains = $this->_config['contain'];
        if (!$this->_checkContainFields($request, $contains)) {
            return false;
        }
        $options = [];
        foreach ($contains as $table => $val) {
            $option = [];
            foreach ($val['fields'] as $column => $name) {
                $option[$column] = $request->getData($name);
            }
            $options[$table] = $option;
        }

        return $this->_findUser(
            $request->getData($fields['username']),
            $request->getData($fields['password']),
            $options
        );
    }
}

また、'finder' => 'auth'を設定しましたので、

Model/Table/UsersTable.php
    public function findAuth(\Cake\ORM\Query $query, array $options)
    {
        $query->where(['Users.status <>' => 'D']);
        return $query;
    }

のようにfindAuthを追加します。
以上で、複数のテーブルをつないで認証を行うことができるようになりました。

参考サイト

CakePHP3:認証