🦊

cakephp2→cakephp5リプレイス_既存のユーザーパスワードを使い回す方法

2024/11/13に公開

はじめに

基本的にパスワードを使い回す方法はセキュリティ上推奨されません。ただ、お客様がどうしてもっていったら仕方ないですよね...
ということでauthenticationを使ってcakephp2の暗号化されたパスワードを用いてログインする方法を記載します。メモ程度です。
cakephpの認証はcakephp4からAuth→Authenticationに変更されています。
ここの変更がかなり大きいので頭抱えるかもしれませんが、一回できれば他のPJでも使いまわせるので頑張ってください。
cakephp2から持ってくる情報はusersのデータベースとセキュリティソルトの値だけです
参考:https://book.cakephp.org/authentication/3/en/password-hashers.html#

セキュリティソルトの変更

セキュリティソルトが流出すると重大インシデントになるのでgitとかにあげたりしないようにしましょう。データがすべてぶっこ抜かれます
cakephp2のセキュリティソルトをコピーする下記

app/Config/core.php
// 略
/**
 * A random string used in security hashing methods.
 */
	Configure::write('Security.salt', 'dammy');
// 略

cakephp5のセキュリティソルトの値を更新する

config/app_local.php
// 略
    /*
     * Security and encryption configuration
     *
     * - salt - A random string used in security hashing methods.
     *   The salt value is also used as the encryption key.
     *   You should treat it as extremely sensitive data.
     */
    'Security' => [
        'salt' => env('SECURITY_SALT', 'dammy'),
    ],
// 略

LegacyPasswordHasher.phpを作成する

src/Auth/LegacyPasswordHasher.php
<?php
namespace App\Auth;

use Cake\Auth\AbstractPasswordHasher;

class LegacyPasswordHasher extends AbstractPasswordHasher
{
    public function hash($password)
    {
        return sha1($password);
    }

    public function check($password, $hashedPassword)
    {
        return sha1($password) === $hashedPassword;
    }
}

Applicationを更新

src/Application.php
<?php

declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link      https://cakephp.org CakePHP(tm) Project
 * @since     3.3.0
 * @license   https://opensource.org/licenses/mit-license.php MIT License
 */

namespace App;

use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Datasource\FactoryLocator;
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\ORM\Locator\TableLocator;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use App\Middleware\HttpBasicAuthMiddleware;
use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Identifier\AbstractIdentifier;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;

/**
 * Application setup class.
 *
 * This defines the bootstrapping logic and middleware layers you
 * want to use in your application.
 *
 * @extends \Cake\Http\BaseApplication<\App\Application>
 */
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();
        $this->addPlugin('Authentication');

        if (PHP_SAPI !== 'cli') {
            FactoryLocator::add(
                'Table',
                (new TableLocator())->allowFallbackClass(false)
            );
        }
    }

    /**
     * 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
            // Catch any exceptions in the lower layers,
            // and make an error page/response
            ->add(new ErrorHandlerMiddleware(Configure::read('Error'), $this))

            // Handle plugin/theme assets like CakePHP normally does.
            ->add(new AssetMiddleware([
                'cacheTime' => Configure::read('Asset.cacheTime'),
            ]))

            // Add routing middleware.
            // If you have a large number of routes connected, turning on routes
            // caching in production could improve performance.
            // See https://github.com/CakeDC/cakephp-cached-routing
            ->add(new RoutingMiddleware($this))

            // Parse various types of encoded request bodies so that they are
            // available as array through $request->getData()
            // https://book.cakephp.org/5/en/controllers/middleware.html#body-parser-middleware
            ->add(new BodyParserMiddleware())

            // Cross Site Request Forgery (CSRF) Protection Middleware
            // https://book.cakephp.org/5/en/security/csrf.html#cross-site-request-forgery-csrf-middleware
            ->add(new CsrfProtectionMiddleware([
                'httponly' => true,
            ]));

        $middlewareQueue->add(new AuthenticationMiddleware($this));

        return $middlewareQueue;
    }

    /**
     * Register application container services.
     *
     * @param \Cake\Core\ContainerInterface $container The Container to update.
     * @return void
     * @link https://book.cakephp.org/5/en/development/dependency-injection.html#dependency-injection
     */
    public function services(ContainerInterface $container): void {}

    public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
    {
        $service = new AuthenticationService();
        $path = $request->getPath();

        $fields = [
            AbstractIdentifier::CREDENTIAL_USERNAME => 'username',
            AbstractIdentifier::CREDENTIAL_PASSWORD => 'password'
        ];

        if (strpos($path, '/admins') === 0) {
            $service->setConfig([
                'unauthenticatedRedirect' => Router::url([
                    'prefix' => 'Admins',
                    'plugin' => null,
                    'controller' => 'Admins',
                    'action' => 'login',
                ]),
                'queryParam' => 'redirect',
            ]);

            $service->loadAuthenticator('Authentication.Session', [
                'sessionKey' => 'AdminUser', //認証情報を保存するセッションキーの変更
            ]);

            $service->loadAuthenticator('Authentication.Form', [
                'fields' => $fields,
                'loginUrl' => Router::url([
                    'prefix' => 'Admins',
                    'controller' => 'Admins',
                    'action' => 'login'
                ]),
            ]);

            $service->loadIdentifier('Authentication.Password', [
                'fields' => $fields,
                'resolver' => [
                    'className' => 'Authentication.Orm',
                    'userModel' => 'Administrators',
                ],
            ]);

            return $service;
        }

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

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

        // Load identifiers
        $service->loadIdentifier(
            'Authentication.Password',
            [
                'fields' => $fields,
                'passwordHasher' => [
                    'className' => 'Authentication.Fallback',
                    'hashers' => [
                        'Authentication.Default',
                        [
                            'className' => 'Authentication.Legacy'
                        ],
                    ]
                ]
            ]
        );

        return $service;
    }
}

主に重要なのが、以下の部分です。ここでcakephp2のパスワードで認証が通るように設計し、かつ、セキュリティ高い暗号化方式でも認証できるようにしています。

// Load identifiers
        $service->loadIdentifier(
            'Authentication.Password',
            [
                'fields' => $fields,
                'passwordHasher' => [
                    'className' => 'Authentication.Fallback',
                    'hashers' => [
                        'Authentication.Default',
                        [
                            'className' => 'Authentication.Legacy'
                        ],
                    ]
                ]
            ]
        );

ログイン処理

cakephp2のパスワードでログインされたらcakephp5のデフォルトの暗号化方式に書き換える処理を入れています。

src/Controller/UsersController.php
<?php

declare(strict_types=1);

namespace App\Controller;

use App\Controller\AppController;
    /**
     * login function
     *
     * ログイン
     *
     * @return \Cake\Http\Response|null|void
     */
    public function login()
    {
        // 後ろに/がつくとログイン失敗するので/抜きにリダイレクトする
        if ($this->getRequest()->getPath() === '/login/') {
            return $this->redirect('/login');
        }
        $errors = [];
        $this->request->allowMethod(['get', 'post']);

        // 直前のURLを取得する
        $referer = $this->referer();
        if (!str_starts_with($referer, '/login')) {
            $this->session()->write('User.referer', $referer);
        }

        // POST, GET を問わず、ユーザーがログインしている場合はリダイレクトします
        $authentication = $this->request->getAttribute('authentication');
        $result = $this->Authentication->getResult();
        if ($result->isValid()) {
            if ($authentication->identifiers()->get('Password')->needsPasswordRehash()) {
                // 過去の暗号化方式のユーザーを同じパスワードで強化したパスワードに変更する
                $user = $this->Users->get($authentication->getIdentity()->getIdentifier());
                $user->password = $this->request->getData('password');
                $this->Users->save($user);
            }
            $user = $this->Authentication->getIdentity();

            $target = $this->Authentication->getLoginRedirect();
            if ($target) {
                return $this->redirect($target);
            }
            // ログイン後に遷移前のページに戻る
            if ($refererUrl = $this->session()->consume('User.referer')) {
                if ($refererUrl) {
                    return $this->redirect($refererUrl);
                }
            }
            return $this->redirect('/');
        }

        // ユーザーが submit 後、認証失敗した場合は、エラーを表示します
        if ($this->request->is('post') && !$result->isValid()) {
            $errors[] = 'IDまたはパスワードが違います';
        }
        $this->set(compact('errors'));
    }

以上
思ったより難しくはないです。

おまけ

prefixがadminのときルート
prfixでRouter使えなくなったので注意

config/routes.php
// 管理者メニュー
    $routes->prefix('Admins', ['_namePrefix' => 'admins:'], function (RouteBuilder $builder) {
        $builder->fallbacks();
        $builder->connect('/login', ['prefix' => 'Admins', 'controller' => 'Admins', 'action' => 'login'], ['_name' => 'login']);

}

Discussion