Symfony Security と JWT の設定方法


[lexik/jwt-authentication-bundle](https://packagist.org/packages/lexik/jwt-authentication-bundle) を利用して API のセキュリティを JWT で構成します。

- Symfony のバージョンは 5.3 を使っています。(5.2 以前とは設定が一部違うようです)
- [MakerBundle](https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html) が必要です。

## jwt-authentication-bundle の導入

[Getting started](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.md#getting-started) の手順に従って導入を進めます。

```shell
composer.phar require lexik/jwt-authentication-bundle
```

上記を実行すると依存関係を解決して、Symfony Security Bundle も導入されます。

flex のおかげで、bundles.php への追加や、各種設定ファイルの更新なども行ってくれます。

### SSL キーの生成

次のコマンドを実行します。

```shell
php bin/console lexik:jwt:generate-keypair
```

*.env* ファイルに次の行が追加されます

```shell
###> lexik/jwt-authentication-bundle ###
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=****************
###< lexik/jwt-authentication-bundle ###
```

*config/packages/lexik_jwt_authentication.yaml* が作成されます。

```yaml
lexik_jwt_authentication:
    secret_key: '%env(resolve:JWT_SECRET_KEY)%'
    public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
    pass_phrase: '%env(JWT_PASSPHRASE)%'
```

これらの設定ファイルが更新された状態から、後ほど設定を変更して行きます。

## ユーザー情報

jwt-authentication-bundle の設定を進める前に、ユーザー情報の作成をしておきます。

ここでは一般的な、 データベースに保存されたユーザー情報を使う Entity プロバイダーを設定します。
ログインユーザー情報を作成します。

### ユーザー Entity の作成

プロジェクトのルートで次のコマンドを実行し、ユーザー Entity を作成します。

```shell
php bin/console make:user
```

実行するとプロンプトが表示されますので、質問に答えます。

```text
The name of the security user class (e.g. User) [User]:
 > User

 Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]:
 > yes

 Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [email]:
 > username

 Will this app need to hash/check user passwords? Choose No if passwords are not needed or will be checked/hashed by some other system (e.g. a single sign-on server).

 Does this app need to hash/check user passwords? (yes/no) [yes]:
 > yes

 created: src/Entity/User.php
 created: src/Repository/UserRepository.php
 updated: src/Entity/User.php
 updated: config/packages/security.yaml
```

User エンティティとリポジトリが作成されます。
同時に、*security.yaml* も更新されます。

テーブルの作成

```shell
bin/console doctrine:schema:update --force
```

もちろん migration を使って作成しても構いません。

### ユーザー情報の登録

ユーザー情報のレコードを登録します。

```shell
php bin/console make:fixtures UserFixtures
```

*src/DataFixtures/UserFixtures.php* が作成されます。
その内容を次のように設定します。

```php
<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;

class UserFixtures extends Fixture
{
    public function __construct(
        private UserPasswordHasherInterface $passwordHasher
    ) {
    }

    public function load(ObjectManager $manager): void
    {
        $user = new User();
        $user
            ->setUsername('admin')
            ->setPassword($this->passwordHasher->hashPassword($user, 'password'))
            ->setRoles(['ROLL_ADMIN', 'ROLL_USER']);
        $manager->persist($user);
        $manager->flush();
    }
}
```

- パスワードをハッシュするために constructor に `UserPasswordHasherInterface` を渡すようにしています。Symfony の DI が注入してくれます
- ユーザーを1件登録します

データをDBに登録させます。

```shell
php bin/console doctrine:fixtures:load
```

```
 Careful, database "foo" will be purged. Do you want to continue? (yes/no) [no]:
 > yes

   > purging database
   > loading App\DataFixtures\UserFixtures
```

以上でユーザー情報の作成は完了です。jwt-authentication-bundle の設定に戻ります。

## `firewalls` の設定

`config/packages/security.yaml` を編集します

```yaml
security:
    enable_authenticator_manager: true

    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        App\Entity\User:
            algorithm: auto

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: username

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login_check
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern:   ^/api
            stateless: true
            jwt: ~

    access_control:
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/api,       roles: IS_AUTHENTICATED_FULLY }
```

[Getting started](https://github.com/lexik/LexikJWTAuthenticationBundle/blob/2.x/Resources/doc/index.md#getting-started) に従って、`firewalls` キーと `access_control` キーを設定しました。

ここまでの設定ができたら、ログイン認証が行えます。

以下のリクエストは JetBrains IDE の http client の形式で記述します。
PHPStorm などで *Tools > Http Client > Create Request in Http Client* で開いた Http Client に貼り付けて実行することができます。

```
POST http://localhost:8000/api/login_check
Content-Type: application/json

{"username":"admin","password":"password"}
```

このリクエストに対して、

```json
{
  "token": "......"
}
```

とレスポンスが返ってきたらとりあえず設定完了です。あとでここで返された token を使います。

簡単な api を作成して呼び出してみます。

`src/Controller/EchoController.php`

```php
<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

class EchoController extends AbstractController
{
    #[Route('/api/echo/{message}',
        name   : 'echo',
        methods: ['GET']
    )]
    public function index(string $message): JsonResponse
    {
        return $this->json([
            'message' => $message
        ]);
    }
}
```

呼び出します。

```http request
GET http://localhost:8000/api/echo/Hello!
```

次のように Unauthorized エラーが返ります。token を渡していないからです。

```json
{
  "code": 401,
  "message": "JWT Token not found"
}
```

`Authorization` ヘッダーにログインの時に返された token を渡します。

```http request
GET http://localhost:8000/api/echo/Hello!
Authorization: Bearer *******ThisIsToken***********
```

次のようにレスポンスが返されるようになります。

```json
{
  "message": "Hello!"
}
```