🕵️

CakePHPの認証の中身を覗く

に公開

概要

CakePHPはデフォルトでは認証機能はありませんが、プラグインを追加することによって認証を追加できます。
認証を利用するにあたって、どのような動いているのかざっくり知りたかったので、中身を追ってみました。
https://book.cakephp.org/authentication/2/ja/index.html

結論

  • PluginとMiddlewareを組み合わせて実現している
  • Middlewareでは認証の実際の処理を実行している
  • Pluginでは、Controllerから認証の結果などを便利に呼び出せるようにしている
  • 認証処理自体はログインしていない画面でもmiddlewareで実行しているため、ログイン処理で認証の結果を $this->Authentication->getResult() で取得できる

詳細

調べようと思った動機

CakePHPの認証を古いAuthComponentからAuthenticationのPluginに変更するにあたって、AuthComponentがデフォルトで提供していた認証処理が、Authentication Pluginでも同様の書き方で動作するのかを理解するため、中身を見てみようと思いました。

処理の中身

大枠

Application.php

// in src/Application.php
class Application extends BaseApplication
    implements AuthenticationServiceProviderInterface
{
// src/Application.php
public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue
{
    $middlewareQueue
        // ... other middleware added before
        ->add(new RoutingMiddleware($this))
        ->add(new BodyParserMiddleware())
        // Add the AuthenticationMiddleware. It should be after routing and body parser.
        ->add(new AuthenticationMiddleware($this));

    return $middlewareQueue;
}

public function getAuthenticationService(ServerRequestInterface $request): AuthenticationServiceInterface
{
    $authenticationService = new AuthenticationService([
        'unauthenticatedRedirect' => Router::url('/users/login'),
        'queryParam' => 'redirect',
    ]);

    // Load the authenticators, you want session first
    $authenticationService->loadAuthenticator('Authentication.Session');
    // Configure form data check to pick email and password
    $authenticationService->loadAuthenticator('Authentication.Form', [
        'fields' => [
            'username' => 'email',
            'password' => 'password',
        ],
        'loginUrl' => Router::url('/users/login'),
        'identifier' => [
            'Authentication.Password' => [
                'fields' => [
                    'username' => 'email',
                    'password' => 'password',
                ],
            ],
        ],
    ]);

    return $authenticationService;
}

AppController.php

// src/Controller/AppController.php
public function initialize(): void
{
    parent::initialize();
    $this->loadComponent('Flash');

    // Add this line to check authentication result and lock your site
    $this->loadComponent('Authentication.Authentication');

それぞれ、どのような意図でどのような内容なのかをざっと見ていきます

Application.php

AuthenticationServiceProviderInterface

AuthenticationServiceProviderInterfaceはコードを見ればわかるのですが、getAuthenticationService の定義がされているのみです。
getAuthenticationService も自分で実装することになるので、Interfaceの継承は必要なのか?というところですが、いったんはそこまで深堀はしません。

https://github.com/cakephp/authentication/blob/3.x/src/AuthenticationServiceProviderInterface.php#L32

getAuthenticationService

AuthenticationService を新しく作成して設定を行います。
loadAuthenticator が認証を行う処理に渡す情報になります。
Authentication.Session であればセッションの値を利用して認証しているかの判定をする
Authentication.Form であれば、ログインのURL、Formの中身、DBのカラム名などを指定してログインの設定を構築できます。
Middlewareなどと同様に、loadAuthenticatorにセットした順番に認証のチェックを行うようになっています。
サンプルの場合、Formの内容がemailがusernameとして扱うカラムにしています。Authentication.Passwordでusernameがemail, passwordがpasswordのカラムであるという定義をしています。
identifierはほかにもいろいろな値が設定できます。ざっくりコメントとして記載しておきます

$identifier = [
    'Authentication.Password' => [
        // passwordを取得するDBのカラム名
        'fields' => [
           'username' => 'email',
           'password' => 'passwd',
       ],
       'resolver' => [
           'className' => 'Authentication.Orm',
           // Modelの指定。デフォルトはUsers
           'userModel' => 'Users',
           // 取得するメソッドの指定。下記のactiveの場合、UsersTableにactiveメソッドが必要になります。
           'finder' => 'active', // default: 'all'
       ],
       'passwordHasher' => [
           'className' => 'Authentication.Fallback',
           'hashers' => [
                'Authentication.Default' => [
                     // passwordのHash化方法。古いHash処理の場合に利用することがあるかと
                    'className' => 'Authentication.Legacy',
                    'hashType' => 'md5',
                ],
            ],
        ],
    ],
];

その他にもいろいろあるので、何かできないかと思ったら以下を参照してみてください。
https://book.cakephp.org/authentication/3/ja/identifiers.html

呼び出しの順番

イメージとしては以下のように呼び出される理解でいいかと思います

  • Middlewareのprocess
    • ControllerのbeforeFiter
    • PluginのbeforeFilter
    • Controllerの実際のメソッド

Middlewareから順番に見ていきます

Middleware メソッド

AuthenticationMiddlewareを追加しています。このMiddlewareはgetAuthenticationService を呼び出して認証の設定を取得して、設定している内容をもとに認証を行います。

MiddlewareではApplication.phpで設定したgetAuthenticationService メソッドを呼び出して、AuthenticationServiceのauthenticateのメソッドを呼び出しています。

https://github.com/cakephp/authentication/blob/3.x/src/Middleware/AuthenticationMiddleware.php#L87

authenticate メソッドでは、Application.phpでloadAuthenticator で設定した認証方法を順番に実行していきます。
よくよく見てみると、認証に成功した後でも、次の認証方法を試しているように見えるのですが、何か理由があるんでしょうかね。。

https://github.com/cakephp/authentication/blob/3.x/src/AuthenticationService.php#L192-L203

AuthenticatorはSession、Form以外にもいろいろあるのですが、試してないのでここでは割愛します。
https://book.cakephp.org/authentication/3/en/authenticators.html

認証を実行した後に、requestのwithAttributeというメソッドを利用して結果を書き込みしています。
Authentication Pluginが利用するために後続の処理に渡しています。

https://github.com/cakephp/authentication/blob/3.x/src/Middleware/AuthenticationMiddleware.php#L101-L103

Middlewareとしての処理はここで終了で、認証のチェックはしたものの、認証していない場合のExceptionはここでは投げません。

ControllerのbeforeFilter

認証をスキップする値の追加は各ControllerのbeforeFilterで記載します。
大元となるAppControllerで書いてもいいですが、フルパスのURIではなく、login のような指定になるので、意図しないURLを許可してしまうのを防ぐためだと思われます。

$this->Authentication->addUnauthenticatedActions(['login']);

Authentication Plugin

認証情報が必要な場合でrequestのattributeに適切な値が入っていない場合はエラーとします。
beforeFilterで呼び出しているdoIdentityCheckという処理で、実際のチェックをしています。

https://github.com/cakephp/authentication/blob/93ef8a1285241c7583154843b17514cabfed3b40/src/Controller/Component/AuthenticationComponent.php#L175-L191

ログイン処理

チュートリアルではログイン処理は以下のように実装されていました。

public function beforeFilter(\Cake\Event\EventInterface $event): void
{
    parent::beforeFilter($event);
    // Configure the login action to not require authentication, preventing
    // the infinite redirect loop issue
    $this->Authentication->addUnauthenticatedActions(['login']);
}

public function login()
{
    $this->request->allowMethod(['get', 'post']);
    $result = $this->Authentication->getResult();
    // regardless of POST or GET, redirect if user is logged in
    if ($result && $result->isValid()) {
        // redirect to /articles after login success
        $redirect = $this->request->getQuery('redirect', [
            'controller' => 'Articles',
            'action' => 'index',
        ]);

        return $this->redirect($redirect);
    }
    // display error if user submitted and authentication failed
    if ($this->request->is('post') && !$result->isValid()) {
        $this->Flash->error(__('Invalid username or password'));
    }
}

ログイン処理では、認証のチェックはするが、beforeFilterで定義している通り、認証していない場合でもエラーにならないように設定しています。
そのため、getResult() を呼び出して認証の結果を取得して、OKかどうかで処理を分岐しています。
OKの場合はSessionに値を設定するなどして認証情報を設定します。

前述のとおり、認証のチェック自体はMiddlewareで実行しているため、ここでは結果の取得のみとなっています。

まとめ

認証の処理はMiddlewareとPluginを組み合わせることで実現をしていることがわかりました。
また、ログイン処理では認証がなくてもエラーとしないが認証のチェック自体はしているため、結果を取得することで、認証が通っているかの確認は結果の取得のみでいいことがわかりました。

簡単にまとめると以下の通りです。

  • 認証が必要な画面かどうかにかかわらず、認証のチェックはMiddlewareで実行している
  • 認証が必要な場合のエラーはPluginで実行している
  • ログイン処理は認証が必要ないが、認証のチェック自体は完了した状態でControllerの該当のメソッドを実行する
DELTAテックブログ

Discussion