[CakePHP3]メール認証を簡単に実装するプラグインを作成した

メールにトークン付きのURLを送信して、メールの認証をすることはアカウント登録時やパスワードのリセットをするときに必要になると思います。
そのときにテーブルにトークンやトークン期限のカラムを追加して....とやる事が多いと思いますがカラム追加を不要にして認証が出来るプラグインを作成しました。

cakephp-token-verify

使い方

簡単なユーザ登録の処理を作成しながら使い方の説明をします。
(モデル側のバリデーション処理などは省いています。)

プラグインインストール

composer require mosaxiv/cakephp-token-verify

テーブル作成

まずはUsersテーブルを作成します。トークンフィールドなどを追加する必要はありません。
メール認証で状態が変わるのでstatusカラムを作成して0が仮登録/1が本登録としておきます。

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    password VARCHAR(255) NOT NULL,
    status INT NOT NULL DEFAULT 0,
    created DATETIME,
    modified DATETIME
);

TokenTrait読み込み

準備はTokenTraitをuseするだけです。
UserエンティティでToken\Model\Entity\TokenTraitをuseします。

app/src/Model/Entity/User.php
use Token\Model\Entity\TokenTrait;

class User extends Entity
{
    use TokenTrait;
}

新規登録の処理をするメソッド作成

UsersControllerにユーザの新規登録処理をするregistrationメソッドを作成します。
registrationでは特に変わったことはしてなく、メールアドレスやパスワードを入力してユーザ登録が完了すればメールを送信しています。

app/src/Controller/UsersController.php
use Cake\Mailer\MailerAwareTrait;

class UsersController extends AppController
{
    use MailerAwareTrait;

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

            if ($this->Users->save($user)) {
                $this->getMailer('User')->send('registration', [$user]);
            }
        }
        $this->set('user', $user);
    }

メール送信

トークンを発行するのはuserエンティティからtokenGenerate()をするだけです。
そのトークンを使ってURLを生成してメールを送信します。

トークンの有効期限はデフォルトでは10分です。指定する場合はtokenGenerate()の引数で分単位で入力できます。

app/src/Mailer/UserMailer.php
namespace App\Mailer;

use Cake\Mailer\Mailer;

class UserMailer extends Mailer
{
    public function registration($user)
    {
        $this
            ->to($user->email)
            ->setSubject('ご登録ありがとうございます。')
            ->set('user', $user);
    }
}
app/src/Template/Email/text/registration.ctp
<?php

use Cake\Routing\Router;

$url = Router::url(['controller' => 'User', 'action' => 'verify', $user->tokenGenerate()], true);
?>
こんにちは、<?= $user->name ?>さん。
メールアドレスを認証をするために以下のURLにアクセスしてください。
<?= $url ?>

トークン認証のメソッド作成

トークン認証用のverifyメソッドを作成します。
Token::getId($token)でトークンからユーザidを取得することが出来ます。取得出来ない場合はfalseが返却されます。
$user->tokenVerify($token)でトークンの有効期限や改ざんを検証することが出来ます。

app/src/Controller/UsersController.php
use Cake\Mailer\MailerAwareTrait;
use Cake\Network\Exception\NotFoundException;
use Token\Util\Token;

class UsersController extends AppController
{
    use MailerAwareTrait;

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

            if ($this->Users->save($user)) {
                $this->getMailer('User')->send('registration', [$user]);
            }
        }
        $this->set('user', $user);
    }

    public function verify($token)
    {
        $user = $this->Users->get(Token::getId($token));
        if (!$user->tokenVerify($token)) {
            throw new NotFoundException();
        }

        // ユーザーステータスを本登録にする。(statusカラムを本登録に更新する)
        $this->Users->activate($user);

        $this->Flash->success('認証完了しました。ログインしてください。');
        return $this->redirect(['action' => 'login']);
    }

テーブルを汚すことなくそこそこ簡単に実装することができました。

仕組み

簡単に仕組みを説明します。

まずトークンとしてjwtを発行しています。jwtの詳細は調べてもらった方がいいと思うので省きますがjwtを使うことで安全性のあるURLセーフなトークンを発行しています。

ワンタイムトークンを作る方法として署名でmodifiedカラムを混ぜることでテーブルが更新されればmodifiedカラム更新され認証を失敗させることにより一度だけ登録できるトークンを作成しています。

Tips

データをこちらでセットすることも出来ます。

$token = $user
      ->setTokenData('type', 'reset_password')
      ->tokenGenerate()

Token::getData($token, 'type') // reset_password