目次

Apricot ユーザ認証

y2sunlight 2020-05-15

Apricot に戻る

関連記事

Apricotでは次の2つのユーザ認証をサポートしています。

セッション認証ではRemember-Meトークンによる自動ログインもサポートしています。これらのユーザ認証の実装は次章以降で行います。

本章の目的は、これらのユーザ認証の基盤を作る事です。その着地点は、シングルトンで動作するユーザ認証クラス( AuthUser )の実装です。このクラスを使って、基本認証またはセッション認証の機能を次章以降で作って行きます。


コアのユーザ認証機能

Apricotのコアで提供するユーザ認証機能は、アプリケーションのモデルやデータベースに依存しない基本的なものだけです。その代わりに、アプリ側とのインターフェース(Authenticatable)を提供します。Authenticatable を使用することで、モデルやデータベースに依存しないユーザ認証機能の基盤を実装することができます。

Authenticatableインターフェース

ユーザ認証インターフェース(Authenticatable)を定義します。

メソッド機能
getAuthName():string認証を識別できる任意の名前を返します。
通常は認証のモデルとなるテーブル名を返します。(例:user)
authenticateUser
(string $account, string $password)
:object|bool
ユーザの認証を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
(アカウントとパスワードによるログイン画面で使用)
remember
(string $remenber_token)
:object|bool
ユーザのRemnber-Meトークンによる認証を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
(RememberMeトークンによる自動ログインで使用)
retrieveUser(object $user)
:object|bool
ユーザオブジェクトの再検索を行います。
成功の場合ユーザオブジェクトを返し、他は false を返します。
再検索のキーは引数 $user のアカウントが使えます。
saveRemenberToken
(object $user, string $remenber_token)
:bool
ユーザのRemnber-Meトークンを保存します。
保存時のキーは引数 $user のアカウントが使えます。

以下に、Authenticatableインターフェースのコードを示します。

/apricot/core/Foundation/Security

Authenticatable.php
<?php
namespace Core\Foundation\Security;
 
/**
 * Authenticatable interface
 */
interface Authenticatable
{
    /**
     * Get authentication name
     * @return string
     */
    public function getAuthName():string;
 
    /**
     * Authenticate user
     * @param string $account
     * @param string $password
     * @return object|bool return object if authenticated, else return false
     */
    public function authenticateUser(string $account, string $password);
 
    /**
     * Remember user
     * @param string $remenber_token
     * @return object|bool return object if authenticated, else return false
     */
    public function rememberUser(string $remenber_token);
 
    /**
     * Retrieve user
     * @param object $user
     * @return object|bool return object if success, else return false
     */
    public function retrieveUser(object $user);
 
    /**
     * Save remember token
     * @param object $user
     * @return bool|bool return true if success, else return false
     */
    public function saveRemenberToken(object $user, string $remenber_token):bool;
}


Authenticationクラス

Authenticatableインターフェースを使ったユーザ認証基本クラス(Authentication)を作ります。Authenticationは以下の公開メソッドを持ちます。

メソッド機能
__construct
(Authenticatable $auth)
Authenticationのコンストラクタ
生成時にAuthenticatableインターフェースを与えます。
authenticate
(string $account, string $password,
bool $remenber=false):bool
ユーザの認証を行い、成功の場合trueを返します。
(アカウントとパスワードによるログイン画面で使用)
remember():boolユーザの自動認証を行い、成功の場合trueを返します。
(RememberMeトークンによる自動ログインで使用)
check():boolユーザが認証されているか否かを返す。
verify():boolユーザが認証されているか否かを返す。
認証されている場合、ログインユーザ情報の更新を行います。
forget()ログインセッションを削除する。
getUser():objectログインユーザを取得する。
getPathAfterLogin():stringログイン後のURLパスを取得する。

本クラスは基本的にセッション認証用に作られていますが、基本認証の場合も authenticate()、verify()、getUser()メソッドは使用できます(基本認証の認証画面はブラウザによって提供され、ログアウトという考え方もなく、セッションとログイン状態は常に一致します)。また、check() と verify() は似たようなメソッドですが、check()はアクションで、verify()はミドルウェアでの使用を想定しています。

Authenticationクラスを以下に示します。

/apricot/core/Foundation/Security

Authentication.php
<?php
namespace Core\Foundation\Security;
 
use Core\Cookie;
use \Core\Session;
 
/**
 * Authentication
 */
class Authentication
{
    /**
     * @var integer
     */
    private const TOKEN_LENGTH = 64;
 
    /**
     * Session key for authenticated user
     */
    private const SESSION_KEY_AUTH = '_auth_';
 
    /**
     * Session key for path after login
     */
    private const SESSION_KEY_PATH_AFTER_LOGIN = '_path_after_login_';
 
    /**
     * Cookie key for remembered user
     */
    private const COOKIE_KEY_REMEMBER = '_remember_';
 
    /**
     * Authentication name
     * @var string
     */
    private $name;
 
    /**
     * Authentication interface
     * @var Authenticatable
     */
    private $auth;
 
    /**
     * Create authentication object
     * @param string $name
     */
    public function __construct(Authenticatable $auth)
    {
        $this->auth = $auth;
        $this->name = $this->auth->getAuthName();
    }
 
    /**
     * Authenticate user (Login)
     * @param string $account
     * @param string $password
     * @param bool $remenber
     * @return bool true if authenticated
     */
    public function authenticate(string $account, string $password, bool $remenber=false): bool
    {
        $user = $this->auth->authenticateUser($account, $password, $remenber);
        if ($user!== false)
        {
            //Set user session
            $this->setUserSession($user, $remenber);
            return true;
        }
        return false;
    }
 
    /**
     * Remember user (Auto Login)
     * @return bool true if authenticated
     */
    public function remember()
    {
        if(Cookie::has($this->getRemenberCookieName()))
        {
            $user = $this->auth->rememberUser(Cookie::get($this->getRemenberCookieName()));
            if (($user!==false))
            {
                // Set user session
                $this->setUserSession($user, true);
                return true;
            }
        }
        return false;
    }
 
    /**
     * Returns whether the user has been authenticated
     * @return bool true if authenticated
     */
    public function check(): bool
    {
        return Session::has(self::SESSION_KEY_AUTH.$this->name);
    }
 
    /**
     * Verify whether user is authenticated
     * @return bool true if authenticated
     */
    public function verify(): bool
    {
        // When alraedy logged in
        if ($this->check())
        {
            $user = $this->getUser();
 
            // Retrieve login user info
            $new_user_info = $this->auth->retrieveUser($user);
 
            // The login user may have been deleted, but keep on login
            if ($new_user_info!==false)
            {
                $this->setUser($new_user_info);
            }
            return true;
        }
 
        // If not authenticated, remember the path after login
        $this->setPathAfterLogin($_SERVER['REQUEST_URI']);
        return false;
    }
 
    /**
     * Forget user's session and cookie
     */
    public function forget()
    {
        // Destroy session completely
        Session::destroy();
 
        // Remove user from cookie
        Cookie::remove($this->getRemenberCookieName());
    }
 
    /**
     * Get authenticated user
     * @return object
     */
    public function getUser()
    {
        return Session::get(self::SESSION_KEY_AUTH.$this->name);
    }
 
    /**
     * Get path after login
     * @return string
     */
    public function getPathAfterLogin() : string
    {
        return Session::get(self::SESSION_KEY_PATH_AFTER_LOGIN.$this->name, route(''));
    }
 
    /**
     * Set user session
     * @param object $user
     * @param bool $remenber
     */
    private function setUserSession(object $user, bool $remenber)
    {
        // Save user in session
        $this->setUser($user);
 
        if ($remenber)
        {
            $remenber_token = $this->getRemenberToken();
 
            // Save remenber_token to DB
            if ($this->auth->saveRemenberToken($user, $remenber_token))
            {
                // Save to cookie
                Cookie::set($this->getRemenberCookieName(), $remenber_token, app('auth.expires_sec'));
            }
        }
        else
        {
            // Remove from cookie
            Cookie::remove($this->getRemenberCookieName());
        }
    }
 
    /**
     * Get login user
     * @return object
     */
    private function setUser(object $user)
    {
        return Session::set(self::SESSION_KEY_AUTH.$this->name, $user);
    }
 
    /**
     * Set path after login
     * @param string $path
     */
    private function setPathAfterLogin(string $path)
    {
        return Session::set(self::SESSION_KEY_PATH_AFTER_LOGIN.$this->name, $path);
    }
 
    /**
     * Get remenber me cookie name
     * @return string
     */
    private function getRemenberCookieName():string
    {
        return self::COOKIE_KEY_REMEMBER.$this->name.'_'.sha1(env('APP_SECRET',self::class));
    }
 
    /**
     * Get remenber me token
     * @return string
     */
    private function getRemenberToken():string
    {
        return str_random(self::TOKEN_LENGTH);
    }
}


アプリ側のユーザ認証共通機能

コアのユーザ認証基本クラス(Authentication)を使用する為には、Authenticatableインターフェースを実装しなければなりません。実装先はモデルクラス(User)が相当と思われますが、モデルに直接コードを書くのではなくて、Authenticatableインターフェースのデフォルト実装をトレイト( AuthTrait )で作ります。こうしておくことで、どんなモデルにも容易にAuthentication インターフェースを実装することができます。

AuthTrait の使い方は以下の様です:

/**
 * Authenticatableを実装するモデルクラス
 */
class FooModel implements Authenticatable
{
    /**
     * Authenticatable User
     * Includeing default implementation of Authenticatable
     */
    use AuthTrait;
 
    ...
}


AuthTraitトレイト

本トレイトはモデルのサブクラスから使用( use )を前提としています。また、AuthTraitではユーザ認証で使用するデータベース情報をアプリケーションの設定ファイル(app.php)から取得しています。app.phpによる設定方法については後述します。

以下に AuthTrait を示します。

/apricot/app/Foundation/Security

AuthTrait.php
<?php
namespace App\Foundation\Security;
 
use ORM;
use Core\Log;
 
/**
 * Authenticatable User
 * Includeing default implementation of Authenticatable
 */
trait AuthTrait
{
    /**
     * Get authentication name
     * {@inheritDoc}
     * @see \Core\Foundation\Security\Authenticatable::getAuthName()
     */
    public function getAuthName(): string
    {
        return $this->tableName();
    }
 
    /**
     * Authenticate user
     * {@inheritDoc}
     * @see \Core\Foundation\Security\Authenticatable::authenticateUser()
     */
    public function authenticateUser(string $account, string $password)
    {
        $table = $this->getAuthName();
        $user = ORM::for_table($table)
        ->where([app("auth.db.{$table}.account")=>$account])
        ->find_one();
 
        if (($user!==false) && (password_verify($password, $user->as_array()[app("auth.db.{$table}.password")])))
        {
            Log::notice("authenticate",[$account]);
            return $user;
        }
 
        return false;
    }
 
    /**
     * Remember user
     * {@inheritDoc}
     * @see \Core\Foundation\Security\Authenticatable::rememberUser()
     */
    public function rememberUser(string $remenber_token)
    {
        $table = $this->getAuthName();
        $user = ORM::for_table($table)
        ->where([app("auth.db.{$table}.remember")=>$remenber_token])
        ->find_one();
 
        if (($user!==false))
        {
            Log::notice("remember",[$user->as_array()[app("auth.db.{$table}.account")]]);
            return $user;
        }
 
        return false;
    }
 
    /**
     * Retrieve user
     * {@inheritDoc}
     * @see \Core\Foundation\Security\Authenticatable::retrieveUser()
     */
    public function retrieveUser(object $user)
    {
        $table = $this->getAuthName();
        $new_user = ORM::for_table($table)
        ->where('account',$user->as_array()[app("auth.db.{$table}.account")])
        ->find_one();
 
        return $new_user;
    }
 
    /**
     * Save remenber token
     * {@inheritDoc}
     * @see \Core\Foundation\Security\Authenticatable::saveRemenberToken()
     */
    public function saveRemenberToken(object $user, string $remenber_token): bool
    {
        $table = $this->getAuthName();
        $pdo = ORM::get_db();
        $sql = "update ".$table.
        " set ".app("auth.db.{$table}.remember")."=?".
        " where ".app("auth.db.{$table}.account")."=?";
        $stmt = $pdo->prepare($sql);
        return $stmt->execute([$remenber_token, $user->as_array()[app("auth.db.{$table}.account")]]);
    }
}


AuthTraitの設定

AuthTrait で使用するデータベース情報の設定は、アプリケーションの設定ファイル(app.php)で行います。

/apricot/config

app.php
<?php
return
[
    'setup' =>[
        ...
    ],
    'middleware' =>[
        ...
    ],
    'csrf' =>[
        ...
    ],
    'auth' =>[
        'db'=>[
            'user'=>[
                'account' =>'account',
                'password' =>'password',
                'remember' =>'remember_token',
            ],
        ],
    ],
];

auth.db の設定はユーザ認証で使用するデータベース(テーブル名とそのカラム)に関する設定です。上記ではテーブル名として user を設定していますが、必要に応じて他のテーブルにすることもできます。


ユーザ認証クラス 

ここまでで、ユーザ認証の準備は終わりました。残りの作業は:

  1. ユーザモデル( User )にAuthTraitトレイトを使ってAuthenticatableインターフェースを実装する
  2. ユーザモデル( User )を使って、Authenticationクラスのシングルトンであるユーザ認証クラス( AuthUser )を生成する

以下で順に説明していきます。

ユーザモデル 

ユーザモデル( User )にAuthenticatableインターフェースを実装します。Authenticatableの実装は、AuthTraitを使用( use )するだけですが、AuthTraitの設定が必要になるので忘れないで下さい。

/apricot/app/Model

User.php
<?php
namespace App\Models;
 
use App\Foundation\Model;
use ORM;
use Core\Foundation\Security\Authenticatable;
use App\Foundation\Security\AuthTrait;
 
/**
 * ユーザモデル
 */
class User extends Model implements Authenticatable
{
    /**
     * Authenticatable User
     * Includeing default implementation of Authenticatable
     */
    use AuthTrait;
 
    ...
}


AuthUserクラス

AuthUserクラスは、コアの Authentication クラスをシングルトンにしたもので、Authenticationのメソッドが全て使用できます。AuthUserクラスは、主にミドルウェアと認証コントローラによって使用されますが、getUser()メソッドは様々な所から呼び出される可能性があります。

使用法: AuthUser::{メソッド}

メソッド機能
__construct
(Authenticatable $auth)
Authenticationのコンストラクタ
生成時にAuthenticatableインターフェースを与えます。
authenticate
(string $account, string $password,
bool $remenber=false):bool
ユーザの認証を行い、成功の場合trueを返します。
(アカウントとパスワードによるログイン画面で使用)
remember():boolユーザの自動認証を行い、成功の場合trueを返します。
(RememberMeトークンによる自動ログインで使用)
check():boolユーザが認証されているか否かを返す。
verify():boolユーザが認証されているか否かを返す。
認証されている場合、ログインユーザ情報の更新を行います。
forget()ログインセッションを削除する。
getUser():objectログインユーザを取得する。
getPathAfterLogin():stringログイン後のURLパスを取得する。

/apricot/core/Foundation/Security

AuthUser.php
<?php
namespace App\Foundation\Security;
 
use Core\Foundation\Singleton;
use Core\Foundation\Security\Authentication;
use App\Models\User;
 
/**
 * User Authentication
 *
 * @method static Authentication getInstance();
 * @method static bool authenticate(string $account, string $password, bool $remenber=false) Authenticate user (Login)
 * @method static void remember() Remember user (Auto Login)
 * @method static bool check() Returns whether the user has been authenticated
 * @method static bool verify() Verify whether user is authenticated
 * @method static void forget() Forget user's session and cookie
 * @method static object getUser() Get authenticated user
 * @method static string getPathAfterLogin() Get path after login
 */
class AuthUser extends Singleton
{
    /**
     * Create user authentication instance.
     * @return \Core\Foundation\Security\Authentication
     */
    protected static function createInstance()
    {
        return new Authentication(new User());
    }
}

マルチ認証

同様の方法で別のモデルを使用したAuthenticationクラスのシングルトンも同時に作る事ができます。例えば、Userとは別に、AdminUserクラスやApiUserクラスも認証したいなどの例には実際によく遭遇するかもしれません。このように、Apricotでは潜在的にマルチ認証を想定した実装に仕上げていますが、これはApricotの範囲を超えているので、これ以上は触れないことにします。


クラスエイリアス

AuthUserクラスをクラスエイリアスに追加します。

/apricot/config/setup

aliases.setup.php
<?php
//-------------------------------------------------------------------
// ビューテンプレートで使うクラスエイリアスを登録
//-------------------------------------------------------------------
return function():bool
{
    $aliases =
    [
        /* Core */
        ....
 
        /* App */
        'ViewHelper' => \App\Helpers\ViewHelper::class,
        'ValidatorErrorBag' => \App\Foundation\ValidatorErrorBag::class,
        'AuthUser' => \App\Foundation\Security\AuthUser::class,
    ];
 
    ....
};