CakePHP2系でハッシュ化されたパスワードをCakePHP3系に更新・移行する方法

web サービスを長期間運用していると、過去に選択したソフトウェアやミドルウェア、また、フレームワークなどが古くなってしまう場合があります。弊社が運用しているフットリンクもリリースしてから数年が経ち、当時最新であった CakePHP のバージョンが古くなってしまいました。そこで、いろんなハードルを乗り越えて一気に CakePHP2.4.x から CakePHP3.4.x にバージョンアップを試みたので、それらで得た知見をご紹介します。

本記事では、認証ロジックのバージョンアップとハッシュ化されたパスワードの移行について記述します。なお、CakePHP2.4.x をベースに記述しましたが、おそらくそれ以前のものでも移行する方法は同じかと思いますので、参考までにご覧下さい。

フットリンクの認証

フットリンクは、会員制のフットサル・サッカー(ソサイチ)のマッチングサイトです。会員制のため、ログインしたユーザしかサイトを利用することができず、認証が必須なサービスとなります。ユーザはメールアドレスとパスワードを入力し、一致した場合のみログインできるという、よくあるサイトです。
パスワードはフレームワークに準拠した方法でハッシュ化されており、直接データベースを参照しても全く理解のできない文字列となっており復号化できない前提となっております。

CakePHP2.4 系の認証ロジックの設定

まずは移行前の CakePHP2.4 系の認証ロジックを調査しました。標準で準備されている AuthComponent を利用しており、AppContorller 内で以下のように記述しております。

# app/Controller/AppController.php

public $components = array(
    'Auth' => array(
        'loginAction' => array(
            'controller' => 'teams',
            'action' => 'login',
        ),
        'loginRedirect' => array(
            'controller' => 'teams',
            'action' => 'home',
        ),
        'logoutRedirect' => array(
            'controller' => 'teams',
            'action' => 'logout',
        ),
        'authError' => array(
            'Auth Error'
        ),
        'authenticate' => array(
            'Form' => array(
                'fields' => array('username' => 'email', 'password' => 'password'),
                'userModel' => 'Team',
                'passwordHasher' => array(
                    'className' => 'Simple',
                    'hashType' => 'sha1'
                )
            )
        ),
    ),
);

重要な箇所は一番最後の authenticate の箇所で、認証に使うモデルは Team、email と password をフォームから送信してログインを試みます。入力された password(平文)は Simple というクラス名の passwordHasher(標準で準備されている SimplePasswordHasher を利用)にてハッシュ化され、方式は sha1 を利用します。

また、平文にて渡されたパスワードをハッシュ化してデータベースに保存する方法として、Team モデルに以下の内容を記述しておりました。

# app/Model/Team.php

public function beforeSave($options = array()) {
    if(!empty($this->data[$this->name]['password'])){
        $passwordHasher = new SimplePasswordHasher();
        $this->data[$this->name]['password'] = $passwordHasher->hash($this->data[$this->name]['password']);
    }
    return true;
}

これらコントローラーやモデルに記述したコードは公式マニュアルと似たような実装となっており、CakePHP2.4 系で構築されたシステムは同じような内容になっている場合が多いかと思います。今回はハッシュ化する方法を確認する必要があるので、SimplePasswordHasher クラスの hash メソッドの中身を参照しました。

SimplePasswordHasher クラスの hash メソッドの内容

SimplePasswordHasher クラスの hash メソッド

public function hash($password) {
    return Security::hash($password, $this->_config['hashType'], true);
}

上記公式サイトの内容を確認すると、Security ライブラリを利用していることがわかりました。同じく Security ライブラリの内容を確認すると、ハッシュ化している箇所を見つけました。

CakePHP2 系のハッシュ化メソッド、Security::hash で呼び出されていた箇所

if ($salt) {
    if (!is_string($salt)) {
        $salt = Configure::read('Security.salt');
    }
    $string = $salt . $string;
}
if (!$type || $type === 'sha1') {
    if (function_exists('sha1')) {
        return sha1($string);
    }
    $type = 'sha256';
}

私は初期利用のままなので、上記条件文の内部にてパスワードの平文がハッシュ化されておりました。Security.salt の値を取得し、その値をパスワード(平文)の文字列の初めに加えて最後に sha1 にてハッシュ化しておりました。

CakePHP2.4 系のハッシュ化まとめ

一旦 CakePHP2.4 系でパスワードをハッシュ化する方法をまとめます。
公式マニュアルに従って特に設定なくパスワードをハッシュ化する場合、方式は sha1 になり、パスワード平文の最初に Security.salt の値を加えてからハッシュ化していることがわかりました。これはのちに記述する Cakephp3 系で過去のハッシュ化されたパスワードを利用する場合に非常に大切な情報となりますので、ご自身の環境のものをよく調査しておいてください。

CakePHP3.4 系の認証ロジック

CakePHP3 系で認証ロジックを実装する場合も、CakePHP2 系と同じく標準で準備されている AuthComponent を利用しました。AppContorller 内で以下のように記述しております。

# app/src/Controller/AppController.php

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

    $this->loadComponent('Auth', [
        'loginAction' => [
            'controller' => 'Teams',
            'action' => 'login',
        ],
        'loginRedirect' => [
            'controller' => 'Teams',
            'action' => 'home'
        ],
        'authenticate' => [
            'Form' => [
                'userModel' => 'Teams',
                'fields' => ['username' => 'email', 'password' => 'password']
            ]
        ]
    ]);
}

CakePHP2.4 系と同様、フォームからメールアドレスとパスワードを送信する方式になります。平文で渡ってきたパスワードをハッシュ化するために、同じく標準で準備されている DefaultPasswordHasher を使用してエンティティにてハッシュ化する処理を記述しました。

# app/src/Model/Entity/Team.php
protected function _setPassword($password)
{
    return (new DefaultPasswordHasher)->hash($password);
}

上記コードに関する具体的な内容は、こちらの記事にまとめております。ハッシュ化してデータベースに保存する処理の解説は先ほどの記事をご確認ください。

CakePHP3.4 系で構築した環境に旧来のハッシュ化ロジックを移植

CakePHP3.4 系で構築した環境に、先ほど調査した CakePHP2.4 系のハッシュ化ロジックを移植します。下記マニュアルに従い、CakePHP2.4 系のハッシュ化クラスを作成しました。なお、CakePHP2.4 系で使用していた Security.salt 値と CakePHP3.4 系で新しく構築した環境の Security.salt 値が異なったため、CakePHP2.4 系のものは Legacy.Security.salt として構成設定し、それをハッシュ化ロジックに利用しました。

公式マニュアル

# app/src/Auth/LegacyPasswordHasher.php

<?php
namespace App\Auth;

use Cake\Auth\AbstractPasswordHasher;

class LegacyPasswordHasher extends AbstractPasswordHasher
{

    public function hash($password)
    {
        return sha1(env('LEGACY_SECURITY_SALT') . $password);
    }

    public function check($password, $hashedPassword)
    {
        return sha1(env('LEGACY_SECURITY_SALT') . $password) === $hashedPassword;
    }
}

2 つのハッシュ化クラスをサイトに設定する

これで、CakePHP3.4 系の新しい環境に、DefaultPasswordHasher(CakePHP3.4 系)と LegacyPasswordHasher(CakePHP2.4 系)の 2 つのパスワードハッシュ化クラスが準備されました。これら 2 つをサイトで利用するために公式マニュアルに従い AppController を下記のように修正します。

ハッシュ化アルゴリズムの変更

'authenticate' => [
    'Form' => [
        'userModel' => 'Teams',
        'fields' => ['username' => 'email', 'password' => 'password'],
        'passwordHasher' => [
            'className' => 'Fallback',
            'hashers' => [
                'Default', 'Legacy'
            ]
        ]
    ]
]

hashers に配列で二つのハッシュ化クラス名を指定し、最初に DefaultPasswordHasher でチェックされます。これに失敗すると、次に LegacyPasswordHasher でチェックされます。公式マニュアルには配列の順番でチェックされると記載されてありましたが、順番を入れ替えても変わらなかった気がします。きちんと検証しきれていませんが、Default が優先的に利用されたように感じました。私は Default が優先順位高い方がいいので無視しましたが、順番の管理が必要な方念のためご注意ください。

ハッシュ化されたパスワードを移行する

ログインそのものは上記設定にて無事できるようになりましたが、このままだと古いハッシュ化アルゴリズムでハッシュ化された値がデータベースに残ってしまいます。したがって、認証が成功した時点で新しいハッシュ化アルゴリズムでハッシュ化された値に更新する処理を加えます。私は公式マニュアルの通り、ログイン処理のアクション内、ログインが成功した時点で更新するように記述しました。

# login アクション内

// login check
$user = $this->Auth->identify();
if ($user) {
    $this->Auth->setUser($user);

    // renewal old password algorithm to new password algorithm
    if ($this->Auth->authenticationProvider()->needsPasswordRehash()) {
        $team = $this->Teams->get($this->Auth->user('id'));
        $team->set('password', $this->request->getData('password'));
        $this->Teams->save($team);
    }
}

フォームから渡ってきたパスワードの平文をエンティティにセットし、それを保存して新しいハッシュ化アルゴリズムでハッシュ化して更新するように記述しました。これで次回からは新しいハッシュ化アルゴリズムで認証が行われるようになります。

終わりに

メジャーバージョンアップはなかなか実行できないかと思いますが、今回私は気合を入れて実行しました。約 2 ヶ月かかりましたが、そこまで込み入ったサービスではないのでこれぐらいの期間で完了しました。CakePHP3 で開発というまとめページに随時更新していますので、CakePHP に関するその他記事もご覧ください。また、何かございましたら遠慮なくお問い合わせください。

Enjoy Develop Life!!