【PHP】PHPフレームワーク「Flow」を試してみる～認証編～

# 初めに
前回は[こちら](https://qiita.com/nnhkrnk/items/ffe50dd90d886f50f73e)の記事で、Flowにおけるイベント管理について書きました。
今回はFlowで**ログイン処理**を実装してみましょう。

# 前提知識
まずは、Flowで認証処理を実装するために抑えておきたい知識を説明します。

## どの認証方式を使うか
認証といっても、**パスワードやキーやSSOなど、様々な種類があります**。
Flowでは、それらの**認証方式の実現するための2つのインターフェース**が用意されています。
* **[1. 認証プロバイダ](https://github.com/neos/flow/blob/8.3/Classes/Security/Authentication/AuthenticationProviderInterface.php)**
* **[2. トークン](https://github.com/neos/flow/blob/8.3/Classes/Security/Authentication/TokenInterface.php)**

### 1. 認証プロバイダ 
認証方式を定義するインターフェースです。

例）パスワード認証、SSOなど

### 2. トークン
認証ステートを管理するためのインターフェースです。
以下の情報を持ちます。
* 認証状態
* どの認証プロバイダを用いているか
* エントリーポイント（ログアウト後に遷移する画面）

などなど

認証プロバイダとトークンは設定ファイルに記載することで、ログイン処理実行時に参照することが可能です。
※後述します。

## 認証専用のActionController
Flowでは、**認証専用のControllerである [AbstractAuthenticationController](https://github.com/neos/flow/blob/8.3/Classes/Security/Authentication/Controller/AbstractAuthenticationController.php)クラス**が用意されており、実装する際は**このクラスを継承して実装する必要**があります。
`AbstractAuthenticationController`には認証に関する以下のメソッドが定義されています。

### 1. authenticateAction
認証を行うためのActionです。
認証の際は、クレデンシャルをformに詰めてこのActionを呼ぶ必要があります。

### 2. onAuthenticationSuccess（※abstract）
ログイン成功時の処理を記載します。
authenticateActionで認証が成功した場合に呼び出されます。

### 3. onAuthenticationFailure
ログイン失敗時の処理を記載します。
authenticateActionで認証が失敗した場合に呼び出されます。

### 4. logoutAction
ログアウトするためのActionです。




# 実際に作成してみる
前提知識がの紹介が終わったので、実際に作ってみましょう。
今回はFlowが提供している認証プロバイダとトークンを用いて、ユーザIDとパスワードを用いた認証を行ってみます。

* **認証プロバイダ：[PersistedUsernamePasswordProvider](https://github.com/neos/flow/blob/8.3/Classes/Security/Authentication/Provider/PersistedUsernamePasswordProvider.php)**
* **トークン：[UsernamePassword](https://github.com/neos/flow/blob/8.3/Classes/Security/Authentication/Token/UsernamePassword.php)**


プロジェクト構成は以下です。

```
Packages/
  ├ Application/
  |    └ Neos.Welcome/
  |         ├ Classes/
  |         |    └ Controller/
  |         |         ├ AccountController.php
  |         |         ├ LoginController.php
  |         |         └ TopController.php
  |         |
  |         ├ Configration/
  |         |    ├ Policy.yaml
  |         |    └ Settings.yaml
  |         |
  |         └ Resources/
  |              └ Private/
  |                   └ Templates/
  |                        ├ Login/
  |                        |   └ index.html
  |                        └ Top/
  |                            └ index.html
  |
  ├ Framework/
  └ Libraries/
```

## 1. アカウントの準備
まずはログインで使用するアカウントを準備します。

### アカウントテーブル
Flowのプロジェクトは、マイグレーション実行時に```neos_flow_security_account```というテーブルをデフォルトで作成します。`PersistedUsernamePasswordProvider`は、このテーブルを用いてユーザのログイン情報を管理します。
（なんのテーブルだろうってずっと疑問でした）

| カラム名                       |説明|
|-----------------------------------|-|
| persistence_object_identifier     |ユニークID|
| accountidentifier                 | アカウントID|
| authenticationprovidername       |**認証プロバイダ名**（★） |
| credentialssource                 |パスワード（暗号化）|
| creationdate                      |作成日時|
| expirationdate                    |有効期限|
| roleidentifiers                   |**権限**（★）|
| lastsuccessfulauthenticationdate  |最終ログイン日時|
| failedauthenticationcount         |ログイン失敗回数|

認証プロバイダ名は、後述するSettings.yamlに定義します。

権限は、Policy.yamlに以下のように定義する必要があります。

```yaml:Policy.yaml
roles:
  'Neos.Welcome:Everybody':
    label: 'everyone'
    description: 'everyone role'
```



### アカウント登録用Controler
アカウント作成用のAPIを作成します。

`neos_flow_security_account`テーブルにINSERTするデータは、`accountFactory`と`accountRepository`というFlowのクラスを用いて作成することができます。
認証プロバイダ名を指定しない場合は、Settings.yamlで定義する`DefaultProvider`が登録されます。

```php:AccountController.php
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Controller\ActionController;

class AccountController extends ActionController
{
    /**
     * @Flow\Inject
     * @var \Neos\Flow\Security\AccountFactory
     */
    protected $accountFactory;

    /**
     * @var \Neos\Flow\Security\AccountRepository
     * @Flow\Inject
     */
    protected $accountRepository;

    /**
     * @Flow\Inject
     * @var \Neos\Flow\Mvc\View\JsonView
     */
    protected $view;

    /**
     * @param string $username
     * @param string $password
     */
    public function registerAction(String $username, String $password)
    {
        $account = $this->accountFactory->createAccountWithPassword($username, $password, array('Neos.Welcome:Everybody'));
        $this->accountRepository->add($account);
        $this->view->assign('value', array('Account created'));
    }
}
```

以下のコマンドでアカウントを登録しました。

```sh
$ curl -i -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "data" {
    "username": "name",
    "password": "pass"
  }
}' \
 'http://localhost:8081/Neos.Welcome/Account/register'
```

## 2. ログイン処理の実装
続いてログイン処理を作っていきます。


### Controller
ログイン画面及びログイン処理を行うControllerです。
`AbstractAuthenticationController`を継承し、各種メソッドを実装します。


```php:LoginController.php
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Security\Authentication\Controller\AbstractAuthenticationController;
use Neos\Flow\Security\Exception\AuthenticationRequiredException;
use Neos\Error\Messages\Error;

class LoginController extends AbstractAuthenticationController
{

    /**
     * ログイン画面表示
     * 
     * @return void
     */
    public function indexAction()
    {}


    /**
     * ログイン成功時の処理
     * 
     * @return string
     */
    protected function onAuthenticationSuccess(ActionRequest $originalRequest = null)
    {
        if ($originalRequest !== null) {
                $this->redirectToRequest($originalRequest);
        }
        $this->redirect('index', 'Top', 'Neos.Welcome');
    }

    /**
     * ログアウト時の処理
     *
     * @return void
     */
    public function logoutAction() {
        parent::logoutAction();
        $this->addFlashMessage('Logout successful');
        $this->redirect('index');
    }

    /**
     * 認証失敗時の処理
     *
     * @return void
     */
    protected function onAuthenticationFailure(AuthenticationRequiredException $exception = null)
    {
        $this->controllerContext->getFlashMessageContainer()->addMessage(new Error('Authentication failed!', ($exception === null ? 1347016771 : $exception->getCode())));
    }
}

```

### HTML

HTMLは、Flow標準のテンプレートエンジン「Fluid」を用いて書きました。
formの送信先はLoginControllerのauthenticateActionメソッドを指定します。LoginControllerが継承しているAbstractAuthenticationControllerで定義されているActionメソッドです。

```html:Login/index.html
<f:layout name="Default" />

<f:section name="Title">Login</f:section>

<f:section name="Content">
    <f:form controller="Login" action="authenticate" method="post">
        <p for="username">Username:</p>
        <f:form.textfield id="username" name="username" /><br/>

        <p for="password">Password:</p>
        <f:form.password id="password" name="password" /><br/>
        <f:form.submit value="Login" />
    </f:form>
</f:section>
```

画面キャプチャはこんな感じ。
ユーザ名とパスワードを入力するシンプルな画面です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/5790d9c3-63ff-8e51-d196-acc2dd00f681.png)


### Sttings.yaml
前述のとおり、設定ファイルには認証プロバイダを記載します。
認証プロバイダ、トークン、エントリーポイントなど、必要な情報を記載します。

```yaml:Settings.yaml
Neos:
  Flow:
    security:
      authentication:
        providers:
          DefaultProvider:
            provider: 'PersistedUsernamePasswordProvider'
            token: 'UsernamePassword'
            tokenOptions:
              usernamePostField: 'username'
              passwordPostField: 'password'
            entryPoint: 'WebRedirect'
            entryPointOptions:
              routeValues:
                '@package': 'Neos.Welcome'
                '@controller': 'Login'
                '@action': 'index'
```

:::note info
providersは複数指定することが可能です。これにより、二段階認証なども実現できそうです。（試せてはないですが）
:::


### Top画面
ログイン後に開くTOP画面を作成します。
特に変わったところはないので、説明は割愛。
<details><summary>ソースはこちら</summary>

```php:TopController.php
<?php
namespace Neos\Welcome\Controller;

use Neos\Flow\Mvc\Controller\ActionController;

class TopController extends ActionController
{
    public function indexAction()
    {
    }
}

```


```haml:Top/index.html
<f:layout name="Default" />
<f:section name="Title">TOP</f:section>
<f:section name="Content">
    <h1>TOP!!!</h1>
</f:section>

```

</details>

## 3. 動作確認
それでは、ログイン成功時と失敗時、それぞれを動作確認してみましょう。

### 3-1. ログイン成功

先ほど登録したアカウントでログインを試みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/ed5bddb2-fcc5-2521-8d8e-e6fe5fd0d934.png)

ログインできました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/73802fc8-d191-204b-1ee0-f4f354869d7d.png)

### 3-2. ログイン失敗

続いて、ログイン失敗してみましょう。
パスワードに適当な文字を入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/4309878c-e25d-5c64-9ba9-9ad5f08d9127.png)

ログインに失敗し、エラーメッセージとともに再度ログイン画面が表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/430841/c7d953b4-7802-2a5b-5a3d-c04830cecf8c.png)

:::note warn
今回は認証の処理を入れただけで、認可の処理は入れていません。そのため、ログインしなくてもTOP画面を開くことができます。
:::


# 終わりに
認証というと難しいイメージがあったんですが、サクッと実装できました。
Flowの認証回りのソースも読み込んだので、もっと詳しい説明や認可処理についても別の記事で書きたいですね。

ここまで読んでいただきありがとうございました！