CakePHP4 で Auth0 を使う。まずは Auth0 に慣れる

Auth0 を使った認証を試してみたいと思います。
まずは、Auth0 の認証（Authentication API）でどのようなことをできるかを試します。

# この記事でわかること

- CakePHP4 での Auth0 の認証（Authentication API）使い方。実践で使うことはできないけど、動きをみることはできる。
- この記事内のソースは以下で公開しています。
    - https://github.com/katsuhiko/cakephp-vue-study


# バージョン情報

|  | バージョン |
|:-----------------|:------------------|
| CakePHP4         | 4.0.5 |
| auth0/auth0-php  | 7.1.0 |


# 事前準備

- [CakePHP4 を docker-compose で動くようにする](https://qiita.com/katsuhiko/items/f18c488457af33abdf66) の記事内容のソースから発展させてます。
- docker-compose を実行し、コンテナを立ち上げます。

```sh
docker-compose up -d
```


# Auth0 の設定

Auth0 のアカウント作成方法は、他サイトを参考にしてください。
アカウント作成後からの手順をメモがわりに残します。


## Application の作成

- 左メニュー「Applications」をクリック
- 画面右「＋ CREATE APPLICATION」ボタンをクリック
- 以下を入力＆選んで「CREATE」ボタンをクリック
    - Name：自身で決めたアプリケーション名を入力
    - Choose an application type：「Regular Web Applications」を選択

これで Application が作成されます。


## Application の設定

- 左メニュー「Applications」をクリック
- 一覧から該当するアプリケーション名リンクをクリック
- タブ「Settings」をクリック
- 以下を入力して「SAVE CHANGES」ボタンをクリック
    - Allowed Callback URLs：「 http://localhost/auth/callback 」を入力
        - Authorization Code Flow での処理で Code を受け取る URL を記載する。
    - Allowed Logout URLs：「 http://localhost/ 」を入力
        - ログアウト後にリダイレクトする（リダイレクト先として指定する） URL を記載する。


# CakePHP4 の実装

## Auth0 のライブラリ追加

Composer を使って Auth0 のライブラリを追加します。
(下の例では、Docker経由で Composer を実行しています。)

```sh
docker exec -it app php composer.phar require auth0/auth0-php
```


## envファイル

Auth0 に関する設定情報を ./config/.env ファイルへ追加します。
追加する設定情報は以下です。

AUTH0_DOMAIN、AUTH0_CLIENT_ID、AUTH0_CLIENT_SECRET は、 Auth0 管理画面（ Auth0 左メニュー「Applications」 > 該当アプリケーション名リンク > タブ「Settings」 ）の Domain、Client ID、Client Secret を記載します。

AUTH0_CALLBACK_URL、AUTH0_LOGOUT_URL は、Auth0 Application 設定でセットした内容です。

```sh:config/.env
# Auth0
export AUTH0_DOMAIN="xxxx.auth0.com"
export AUTH0_CLIENT_ID="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AUTH0_CLIENT_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
export AUTH0_CALLBACK_URL="http://localhost/auth/callback"
export AUTH0_LOGOUT_URL="http://localhost/"
```

他の人にも展開することを考えて、 ./config/.env.example へも同様の記述を残していきます。
秘密的な情報はあまり記載したくないので、自分は以下の内容にしています。

```sh:config/.env.example
# Auth0
export AUTH0_DOMAIN="example.auth0.com"
export AUTH0_CLIENT_ID=""
export AUTH0_CLIENT_SECRET=""
export AUTH0_CALLBACK_URL="http://localhost/auth/callback"
export AUTH0_LOGOUT_URL="http://localhost/"
```


## AuthController の実装

最後に Controller の実装の全体は載せます。ここではメソッド単位で説明します。


### Auth0 インスタンス作成

Auth0 のインスタンスを作るメソッドです。

RefreshTokenが必要な場合、 `scope` へ `offline_access` を追加する必要があります。

`persist_*` のパラメータは、Auth0から取得した情報を Store（デフォルトだとセッション情報）へ保持するかです。
 user はデフォルト保持なので、保持したくない場合 `false` を指定します。
その他の token 系情報は、デフォルト保持しないなので、保持したい場合 `true` を指定します。

Auth0 のインスタンスを作るときのパラメータはたくさんあります。詳しくはソースを見るのが手っ取り早いです。

- https://github.com/auth0/auth0-PHP/blob/master/src/Auth0.php#L216

```php:src/Controller/AuthController.php
    /**
     * @return \Auth0\SDK\Auth0
     */
    private function auth0(): Auth0
    {
        return new Auth0([
            'domain' => env('AUTH0_DOMAIN', ''),
            'client_id' => env('AUTH0_CLIENT_ID', ''),
            'client_secret' => env('AUTH0_CLIENT_SECRET', ''),
            'redirect_uri' => env('AUTH0_CALLBACK_URL', ''),

            // refresh token が必要な場合は、scope へ offline_access を追加する
            // @see https://auth0.com/docs/tokens/guides/get-refresh-tokens
            'scope' => 'openid profile email',
            // 'scope' => 'openid profile email offline_access',

            // exchange (code から access token を取得) 後、ユーザー情報を取得する場合は true とする
            // access token と一緒に返却される id token(JWT) を decode したユーザー情報でよければ false (デフォルト) とする
            // 'skip_userinfo' => false,

            // access token を Store (Session情報) へ保持しない場合は false とする
            // 'persist_user' => false,

            // access token を Store (Session情報) へ保持する場合は true とする
            'persist_access_token' => true,

            // refresh token を Store (Session情報) へ保持する場合は true とする
            // 'persist_refresh_token' => true,

            // id token を Store (Session情報) へ保持する場合は true とする
            // 'persist_id_token' => true,
        ]);
    }
```


### login の実装

Auth0 のログイン画面を表示するアクションです。

`$auth0->login()` でも良いのですが、呼び出したメソッド先で勝手に exit されるのが気持ち悪く感じたので、あえて `$auth0->getLoginUrl()` でリダイレクトURLを取得して、自身でリダイレクトを実行させています。

それと、 Code を受け取るときの Callback でエラーを発生させないために、ここで Session クリアを実施しています。

```php:src/Controller/AuthController.php
    /**
     * @return \Cake\Http\Response|null
     */
    public function login(): ?Response
    {
        // Session を破棄して、Auth0のユーザー情報を空にする。
        // そうしないと、Auth0->exchange() で
        // Can't initialize a new session while there is one active session already
        // が発生する。
        $this->getRequest()->getSession()->destroy();

        $auth0 = $this->auth0();

        $loginUrl = $auth0->getLoginUrl();

        return $this->redirect($loginUrl);
    }
```

### callback の実装

Authorization Code Flow での処理で Code を受け取るアクションです。

Code から AccessToken / IdToken を取得する `$auth0->exchange()` を呼び出します。
queryパラメータからの Code の取得や queryパラメータの State のチェックは `$auth0->exchange()` のメソッド内で適切に実施してくれます。

デフォルトの場合、State のチェックは Cookie を使っており、チェック時に Cookie から値を削除するため、２度 `$auth0->exchange()` を呼び出すと Stateが不正である旨のエラーが発生します。気を付けてください。

```php:src/Controller/AuthController.php
    /**
     * @return \Cake\Http\Response|null
     */
    public function callback(): ?Response
    {
        $auth0 = $this->auth0();

        $auth0->exchange();

        return $this->render();
    }
```


### cache の実装

Auth0 が Store（デフォルトセッション情報）に保持している内容を確認します。
今回の Auth0 のインスタンスを作るときの設定であれば、 `$auth0->getUser()` `$auth0->getAccessToken()` は Store に保持されており、残りは保持されていません。

```php:src/Controller/AuthController.php
    /**
     * @return \Cake\Http\Response|null
     */
    public function cache(): ?Response
    {
        $auth0 = $this->auth0();

        debug($auth0->getUser());
        debug($auth0->getAccessToken());
        debug($auth0->getIdToken());
        debug($auth0->getRefreshToken());

        return $this->render();
    }
```


### user の実装

Authentication API を使ってユーザー情報を取得します。
このときにログインユーザーの AccessToken を使います。

```php:src/Controller/AuthController.php
    /**
     * @return \Cake\Http\Response|null
     */
    public function user(): ?Response
    {
        $auth0 = $this->auth0();

        /** @var string $domain */
        $domain = env('AUTH0_DOMAIN', '');
        /** @var string $clientId */
        $clientId = env('AUTH0_CLIENT_ID', '');
        $authApi = new Authentication($domain, $clientId);

        /** @var string $accessToken */
        $accessToken = $auth0->getAccessToken();
        $user = $authApi->userinfo($accessToken);

        debug($user);

        return $this->render();
    }
```


### logout の実装

Authentication API を使ってログアウトURLを取得します。

```php:src/Controller/AuthController.php
    /**
     * @return \Cake\Http\Response|null
     */
    public function logout(): ?Response
    {
        $auth0 = $this->auth0();

        $auth0->logout();

        /** @var string $domain */
        $domain = env('AUTH0_DOMAIN', '');
        /** @var string $clientId */
        $clientId = env('AUTH0_CLIENT_ID', '');
        $authApi = new Authentication($domain, $clientId);

        /** @var string $returnTo */
        $returnTo = env('AUTH0_LOGOUT_URL', '');
        $logoutUrl = $authApi->get_logout_link($returnTo, $clientId);

        return $this->redirect($logoutUrl);
    }
```

AuthController の実装は以上です。


## routes の定義

AuthContoller のルートを定義します。
Code を受け取る callback URL は、Auth0 Application にセットした内容に合わせる必要があります。

```php:config/routes.php
// ... snip
$routes->scope('/', function (RouteBuilder $builder) {
    // Register scoped middleware for in scopes.
    $builder->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true,
    ]));

    $builder->applyMiddleware('csrf');

    $builder->connect('/login', ['controller' => 'Auth', 'action' => 'login']);
    $builder->connect('/logout', ['controller' => 'Auth', 'action' => 'logout']);
    $builder->connect('/auth/callback', ['controller' => 'Auth', 'action' => 'callback']);
    $builder->connect('/auth/user', ['controller' => 'Auth', 'action' => 'user']);
    $builder->connect('/auth/cache', ['controller' => 'Auth', 'action' => 'cache']);
    // ... snip
});
// ... snip
```

これで

- ログイン( http://localhost/login )
- ログインユーザー情報取得( http://localhost/auth/cache , http://localhost/auth/user )
- ログアウト( http://localhost/logout )

の動きを確認することができます。


# 補足

## Auth0 でGoogleソーシャルログインをさせない

Auth0 の Applications から設定できます。

- 左メニュー「Applications」をクリック
- 一覧から該当する Applicationリンクをクリック
- タブ「Connections」をクリック
- Social > google-oauth2 を OFF にする。


## Auth0 でサインアップをさせない

Auth0 の Connections から設定できます。

- 左メニュー「Connections」 > 「Database」をクリック
- 一覧から該当するDatabase Connections(「Username-Password-Authentication」)をクリック
- 「Disable Sign Ups」を ON にする。


## Auth0 のログイン画面を日本語にする

Auth0 の Tenant Settings から設定できます。

- 右上ログインアカウントプルダウンメニュー「Settings」をクリック
- Default Language：「Japanese（ja）」を選択して「SAVE」をクリック


## AuthController の全体ソース


```php:src/Controller/AuthController.php
<?php
declare(strict_types=1);

namespace App\Controller;

use Auth0\SDK\API\Authentication;
use Auth0\SDK\Auth0;
use Cake\Http\Response;

/**
 * Auth Controller
 */
class AuthController extends AppController
{
    /**
     * @return \Auth0\SDK\Auth0
     */
    private function auth0(): Auth0
    {
        return new Auth0([
            'domain' => env('AUTH0_DOMAIN', ''),
            'client_id' => env('AUTH0_CLIENT_ID', ''),
            'client_secret' => env('AUTH0_CLIENT_SECRET', ''),
            'redirect_uri' => env('AUTH0_CALLBACK_URL', ''),

            // refresh token が必要な場合は、scope へ offline_access を追加する
            // @see https://auth0.com/docs/tokens/guides/get-refresh-tokens
            'scope' => 'openid profile email',
            // 'scope' => 'openid profile email offline_access',

            // exchange (code から access token を取得) 後、ユーザー情報を取得する場合は true とする
            // access token と一緒に返却される id token(JWT) を decode したユーザー情報でよければ false (デフォルト) とする
            // 'skip_userinfo' => false,

            // access token を Store (Session情報) へ保持しない場合は false とする
            // 'persist_user' => false,

            // access token を Store (Session情報) へ保持する場合は true とする
            'persist_access_token' => true,

            // refresh token を Store (Session情報) へ保持する場合は true とする
            // 'persist_refresh_token' => true,

            // id token を Store (Session情報) へ保持する場合は true とする
            // 'persist_id_token' => true,
        ]);
    }

    /**
     * @return \Cake\Http\Response|null
     */
    public function login(): ?Response
    {
        // Session を破棄して、Auth0のユーザー情報を空にする。
        // そうしないと、Auth0->exchange() で
        // Can't initialize a new session while there is one active session already
        // が発生する。
        $this->getRequest()->getSession()->destroy();

        $auth0 = $this->auth0();

        $loginUrl = $auth0->getLoginUrl();

        return $this->redirect($loginUrl);
    }

    /**
     * @return \Cake\Http\Response|null
     */
    public function logout(): ?Response
    {
        $auth0 = $this->auth0();

        $auth0->logout();

        /** @var string $domain */
        $domain = env('AUTH0_DOMAIN', '');
        /** @var string $clientId */
        $clientId = env('AUTH0_CLIENT_ID', '');
        $authApi = new Authentication($domain, $clientId);

        /** @var string $returnTo */
        $returnTo = env('AUTH0_LOGOUT_URL', '');
        $logoutUrl = $authApi->get_logout_link($returnTo, $clientId);

        return $this->redirect($logoutUrl);
    }

    /**
     * @return \Cake\Http\Response|null
     */
    public function callback(): ?Response
    {
        $auth0 = $this->auth0();

        $auth0->exchange();

        return $this->render();
    }

    /**
     * @return \Cake\Http\Response|null
     */
    public function user(): ?Response
    {
        $auth0 = $this->auth0();

        /** @var string $domain */
        $domain = env('AUTH0_DOMAIN', '');
        /** @var string $clientId */
        $clientId = env('AUTH0_CLIENT_ID', '');
        $authApi = new Authentication($domain, $clientId);

        /** @var string $accessToken */
        $accessToken = $auth0->getAccessToken();
        $user = $authApi->userinfo($accessToken);

        debug($user);

        return $this->render();
    }

    /**
     * @return \Cake\Http\Response|null
     */
    public function cache(): ?Response
    {
        $auth0 = $this->auth0();

        debug($auth0->getUser());
        debug($auth0->getAccessToken());
        debug($auth0->getIdToken());
        debug($auth0->getRefreshToken());

        return $this->render();
    }
}
```