【cakephp3】Kintoneに保存されたユーザー情報を使用して認証を行う

# やりたいこと
cakephp3でログイン機能を実装する際、認証情報として使用するユーザーデータとしてKintone上のアプリケーションに登録されたユーザー情報を使いたい。

ちなみに今回使ったのがKintoneというだけで、ユーザー情報の場所が変わってもここで説明したやり方を応用すれば対応可能です。

# 参考にしたページ
フレームワークのことなら[公式サイト](https://book.cakephp.org/3.0/ja/controllers/components/authentication.html)を見ればおおよそ解決!みんな知ってるね!

# 環境
php => 7.3.6
cakephp => 3.8.0

# 前提
* ユーザー情報が登録されているアプリケーションがKintone上に存在している(項目はとりあえずユーザー名とパスワードのみあれば可。また、passwordは登録時にハッシュ化されていること)

# やってみよう
というわけで実際にどうやるのかを説明していきますが、今回の場合は大まかに分けて手順は二つです。

1. cakephpの `Cake\Auth\BaseAuthenticate` を継承したKintone認証用のクラスを作成する。
2. 1で作成した認証用クラスをAuthコンポーネントの呼び出し時に認証クラスとして使用されるように設定する。

トテモカンタンネ。

## Kintone認証用のクラスを作成
```src/Auth/KintoneAuthenticate.php
<?php

namespace App\Auth;

use Cake\Auth\BaseAuthenticate;
use Cake\Http\Response;
use Cake\Http\ServerRequest;
use CybozuHttp\Api\KintoneApi;
use CybozuHttp\Client;

/**
 * Kintone上に登録された情報での認証を行う
 */
class KintoneAuthenticate extends BaseAuthenticate
{
    public function authenticate(ServerRequest $request, Response $response)
    {
        $fields = $this->_config['fields'];
        if (!$this->checkFields($request, $fields)) {
            return false;
        }

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

    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 _findUser($username, $password = null)
    {
        if ($password === null) {
            return false;
        }

        $client = new KintoneApi(
            new Client(
                [
                    'domain' => {ドメイン},
                    'subdomain' => {サブドメイン},
                    'use_api_token' => true,
                    'token' => {apiトークン},
                ]
            )
        );

        $query = "ユーザー名 = \"" . $username . "\"";

        $response = $client->records()->get(
            {kintone上のアプリケーションid},
            $query
        );

        if (count($response['records']) < 1) {
            return false;
        }

        $record = $response['records'][0];

        // パスワードハッシュ用のクラスを取得
        $hasher = $this->passwordHasher();
        $hashedPassword = $record['パスワード']['value'];

        // 入力されたパスワードとハッシュ化されたパスワードが一致するか確認
        if (!$hasher->check($password, $hashedPassword)) {
            return false;
        }

        $this->_needsPasswordRehash = $hasher->needsRehash($hashedPassword);

        $fields = $this->_config['fields'];

        // ユーザー情報を返す
        return [
            $fields['username'] => $username,
            $fields['password'] => $hashedPassword
        ];
    }
}
```

色々書いてありますが、要は `Cake\Auth\BaseAuthenticate` を継承した認証用クラスを作成し、 `authenticate` 内で実際の認証処理を書いてあげるだけです。
その際の返却値は、失敗の場合はfalse、成功した場合はユーザー情報を配列で返します。

また、このサンプルではKintoneからのデータ取得用に [ochi51/cybozu-http](https://packagist.org/packages/ochi51/cybozu-http)を使用していますので、こちらの使い方がわからない方は各自別途検索又は自分の使用している取得手段にそれぞれ置き換えしてください。
ユーザー情報取得についてはほぼライブラリで行なっているので、パスワードの比較の部分をもう少し詳細に説明します。


```php
// パスワードハッシュ用のクラスを取得
$hasher = $this->passwordHasher();
$hashedPassword = $record['パスワード']['value'];

// 入力されたパスワードとハッシュ化されたパスワードが一致するか確認
if (!$hasher->check($password, $hashedPassword)) {
    return false;
}
```

上記の `$this->passwordHasher()` でパスワードハッシュ用のクラスを取得できます。
当然データ登録時に使用したハッシュクラスでないと正しくパスワードの比較が行えないため、気をつけてください。
今回は特に何も指定していないため、 `Cake\Auth\DefaultPasswordHasher` が使用されています。
その後 `check()` でパスワードの一致を行なっていますが、 `DefaultPasswordHasher` ではパスワードハッシュの処理と比較の処理は下記のように定義されています。

```php:Cake\Auth\DefaultPasswordHasher
// ハッシュ時の処理
public function hash($password)
{
    return password_hash(
        $password,
        $this->_config['hashType'],
        $this->_config['hashOptions']
    );
}

// 比較時の処理
public function check($password, $hashedPassword)
{
    return password_verify($password, $hashedPassword);
}
```

内部的にはphpの組み込み関数である `password_hash()` と `password_verify()` が呼ばれているだけですね。

```php
// ユーザー情報を返す
return [
    $fields['username'] => $username,
    $fields['password'] => $hashedPassword
];
```

続いてログインが成功した場合に返す情報ですが、現在ログインしているユーザー情報(Contollerで `$this->Auth->user()` を使用した時に取れる情報)としてセッションに入るので、必要な情報は全て入れておきましょう。

## Authコンポーネントの呼び出し時に認証クラスとして使用されるように設定する
これは非常に簡単。

```php
<?php

namespace App\Controller;

class MembersController extends AppController
{
    public function initialize()
    {
        parent::initialize();
        $this->loadComponent('Auth', [
            'authenticate' => [
                'Kintone' => [
                    'fields' => [
                        'username' => 'username',
                        'password' => 'password',
                    ],
                ],
            ],
        ]);
    }
}
```

これだけでOKです。
`$this->loadComponent()` でAuthComponentを読み込む際の第二引数に、 `authenticate` というキー名で使用したい認証クラス名を渡してあげます( `authenticate` の部分は不要です。)

`fields` でも設定が入っていますが、ユーザー名とパスワードが何というキー名で渡ってくるかを設定しています。
今回は省いていますが、ctp側でそれぞれキー名と同じ `username` と `password` という名前で渡すようにしているためキー名と値は一緒になっています。
認証クラスの作成時に `$this->_config['fields']` という記述があったとおもいますが、それはこの設定を取得していたんですね。

ここまで設定したらログインフォームから値を入力し、実際にログイン処理を試してみてください。
kintone上のユーザー情報を元にログイン認証を行うことができるようになっているはずです。

# 余談
今回認証用クラスを作成するにあたって、既存の `Cake\Auth\BaseAuthenticate` 内の処理をみていたのですが、その中のユーザー取得用関数にこんな記述が

```php:Cake\Auth\BaseAuthenticate
protected function _findUser($username, $password = null)
{
    $result = $this->_query($username)->first();

    if (empty($result)) {
        // Waste time hashing the password, to prevent
        // timing side-channels. However, don't hash
        // null passwords as authentication systems
        // like digest auth don't use passwords
        // and hashing *could* create a timing side-channel.
        if ($password !== null) {
            $hasher = $this->passwordHasher();
            $hasher->hash($password);
        }

        return false;
    }
// ...(略)
```

データを取得した結果が空の場合、パスワードがあればとりあえずハッシュ・・・?
コメント見る限りサイドチャネル攻撃の対応っぽいことが書いてあり、多少の推測はできるのですが細かい理屈がよくわからなかったです。

あと今回要点だけに絞って大分簡略化して説明書いちゃいましたが、もしデータ登録とか含めた全体見たいとか要望多ければそちらも書くかも。

<br>
<br>
<br>
<br>
ぼく「これで僕もAuthComponentを使用した様々な認証タイプに対応できるようになったぞ!」
cake君「4系からAuthComponent非推奨だぞ」
<img width="501" alt="スクリーンショット 2019-08-28 2.56.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/236058/82ef53b4-6fad-2139-aff3-4e4bb98ea599.png">
ぼく「あっ、ふ〜ん」(絶望)
４系楽しみですね。