丸善印刷株式会社

ブログ

ニュース & ブログ

Symfonyで2段階認証【Google Authenticator】

公開日:2021.08.09

 

以前、「2段階認証には「Authy」がおすすめ」という記事で、2段階認証のクライアントアプリについて書きましたが、 今回はWebアプリケーション側に2段階認証の機能を組み込んでみましたので、その方法をご紹介したいと思います。

利用するのは「Symfonyのバージョン 5.3.6」です。

まずは、Symfonyで簡易的に、ログインフォーム、認証・認可機能、ユーザー登録フォームを作成し、 その後、2段階認証の機能を追加していきます。

2段階認証で利用するSymfonyバンドル

2段階認証機能は「scheb/2fa」というSymfonyバンドルを利用し、その他、「scheb/2fa-google-authenticator」「scheb/2fa-qr-code」というバンドルも利用します。

「scheb/2fa」

https://github.com/scheb/2fa

「scheb/2fa」は2段階認証のコア機能だけを提供します。実現したいことにより、追加バンドルを入れる必要があります。 2段階認証の方法としては、「TOTP認証」「Google Authenticator」「メールでの認証」の3つの方法が用意されており 今回は「Google Authenticator」を利用します。

「scheb/2fa-google-authenticator」

https://github.com/scheb/2fa/blob/5.x/doc/providers/google.md

「Google Authenticator」を認証方法として利用する場合の追加バンドルです。

「scheb/2fa-qr-code」

2段階認証のクライアントアプリに読み込ませるQRコードを生成するための追加バンドルです。

ログインフォーム、認証・認可機能、ユーザー登録フォームを作成

では、2段階認証機能を追加する前に、簡易的なログインフォーム、認証・認可機能、ユーザー登録フォームを作成します。

基本的には、下記URLの内容に沿って作成をしています。

 https://symfony.com/doc/current/security.html

ログインフォーム、認証・認可機能を作成

ユーザー用のエンティティを作成します。

$ bin/console make:user

 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]:
 > email

 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

...

マイグレーションでユーザー用のテーブルを作成します。

$ bin/console make:migration
$ bin/console doctrine:migrations:migrate

マイグレーションを実行すると、次のようなテーブルが作成されます。

mysql> desc user;
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| id       | int(11)      | NO   | PRI | NULL    | auto_increment |
| email    | varchar(180) | NO   | UNI | NULL    |                |
| roles    | json         | NO   |     | NULL    |                |
| password | varchar(255) | NO   |     | NULL    |                |
+----------+--------------+------+-----+---------+----------------+
4 rows in set (0.00 sec)

ログイン後に表示するページとして「/admin」を作成します。

$ bin/console make:controller

 Choose a name for your controller class (e.g. BraveElephantController):
 > Admin

 created: src/Controller/AdminController.php
 created: templates/admin/index.html.twig


  Success!


まだ、認証・認可の制限を入れていないので、先ほどほど作成した「/admin」ページにブラウザでアクセスすると、ページの内容が表示されます。

http://localhost/admin

 

ログインフォームを作成します。

$ bin/console make:auth

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 1

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > LoginFormAuthenticator

 Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 > SecurityController

 Do you want to generate a '/logout' URL? (yes/no) [yes]:
 > yes

 created: src/Security/LoginFormAuthenticator.php
 updated: config/packages/security.yaml
 created: src/Controller/SecurityController.php
 created: templates/security/login.html.twig


  Success!


 Next:
 - Customize your new authenticator.
 - Finish the redirect "TODO" in the App\Security\LoginFormAuthenticator::onAuthenticationSuccess() method.
 - Check the user's password in App\Security\LoginFormAuthenticator::checkCredentials().
 - Review & adapt the login template: templates/security/login.html.twig.

 

ログイン成功後のリダイレクト先を「/admin」に設定します。

src/Security/LoginFormAuthenticator.php

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
            return new RedirectResponse($targetPath);
        }

        // For example:
        //return new RedirectResponse($this->urlGenerator->generate('some_route'));
+       return new RedirectResponse($this->urlGenerator->generate('admin'));
        throw new \Exception('TODO: provide a valid redirect inside '.__FILE__);
    }

 

「/admin」ページに認可設定を入れます。 「/admin」へのアクセスには「ROLE_USER」権限が必要としました。 「ROLE_USER」はデフォルトのロールなので、ログインにしたユーザーは誰でも「/admin」が閲覧できます。

config/packages/security.yaml

    access_control:
        - { path: ^/admin, roles: ROLE_USER }

 

この状態で「/admin」にアクセスをすると、ログインフォームに飛ばされます。

 

 

これで、ログインフォーム、認証・認可機能は完成しました。この後、ユーザー登録フォームを作成していきます。

ユーザー登録フォームを作成

ユーザー登録用のフォームを作成します。

$ bin/console make:registration-form

 Creating a registration form for App\Entity\User

 Do you want to add a @UniqueEntity validation annotation on your User class to make sure duplicate accounts aren't created? (yes/no) [yes]:
 > yes

 Do you want to send an email to verify the user's email address after registration? (yes/no) [yes]:
 > no

 Do you want to automatically authenticate the user after registration? (yes/no) [yes]:
 > no

 What route should the user be redirected to after registration?:
  [0 ] _wdt
  [1 ] _profiler_home
  [2 ] _profiler_search
  [3 ] _profiler_search_bar
  [4 ] _profiler_phpinfo
  [5 ] _profiler_search_results
  [6 ] _profiler_open_file
  [7 ] _profiler
  [8 ] _profiler_router
  [9 ] _profiler_exception
  [10] _profiler_exception_css
  [11] admin
  [12] app_login
  [13] app_logout
  [14] _preview_error
 > 12

 updated: src/Entity/User.php
 created: src/Form/RegistrationFormType.php
 created: src/Controller/RegistrationController.php
 created: templates/registration/register.html.twig


  Success!


 Next:
 Make any changes you need to the form, controller & template.

 Then open your browser, go to "/register" and enjoy your new form!

ユーザー登録用フォームのURLは、「http://localhost/register」となります。

このフォームからユーザーの登録をします。

 

 

DBには次のように登録されます。

ロールの設定はありませんが、デフォルトの「ROLE_USER」ロールは使えるので、「/admin」へのアクセスは可能です。

mysql> select * from user;
+----+-------------------+-------+--------------------------------------------------------------+
| id | email             | roles | password                                                     |
+----+-------------------+-------+--------------------------------------------------------------+
|  1 | test@maruzen-p.jp | []    | $2y$13$TKrKI4A8je1Qxn2wlw3nNeqzUeX9cYDXynW2dUDhOemD5I9jfyg3W |
+----+-------------------+-------+--------------------------------------------------------------+
1 row in set (0.00 sec)

 

ログイン画面で、先ほど登録した、メールアドレスとパスワードを入力します。

 

ログインに成功し、「/admin」ページが表示されます。

 

これで、ログインフォーム、認証・認可機能、ユーザー登録フォームが完成しました。

この後、本題の2段階認証の機能を作成していきます。

 

2段階認証の基本機能を追加

では、「scheb/2fa-bundle」を使って、2段階認証の機能を作成していきます。

基本、下記マニュアルを参考としております。

https://github.com/scheb/2fa/blob/5.x/doc/installation.md

「scheb/2fa-bundle」を追加

composerで「scheb/2fa-bundle」を追加します。

$ composer require scheb/2fa-bundle

上記バンドルを追加すると、「config/packages/scheb_2fa.yaml」と「config/routes/scheb_2fa.yaml」というファイルが作成されます。


config/packages/scheb_2fa.yaml

# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
    security_tokens:
        # - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
        # If you're using guard-based authentication, you have to use this one:
        # - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
        # If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
        - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken

「Symfony5.3」では、「security_tokens」は「authenticator-based security」を使うので、一番最後の行をコメントアウトします。

 

「config/routes/scheb_2fa.yaml」で、以下のルートが登録されます。

$ bin/console debug:route | grep 2fa
  2fa_login                  ANY      ANY      ANY    /2fa
  2fa_login_check            ANY      ANY      ANY    /2fa_check

firewallの設定

「firewall」に2段階認証用の設定を追加し、「access_control」に2段階認証で利用するpathを追加します。

config/packages/security.yaml

...
firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    main:
        lazy: true
        provider: app_user_provider
        custom_authenticator: App\Security\LoginFormAuthenticator
+       two_factor:
+           auth_form_path: 2fa_login    # The route name you have used in the routes.yaml
+           check_path: 2fa_login_check  # The route name you have used in the routes.yaml
        logout:
            path: app_logout
...

access_control:
+   - { path: ^/logout, role: IS_AUTHENTICATED_ANONYMOUSLY }
+   - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
    - { path: ^/admin, roles: ROLE_USER }

 

「Google Authenticator」用の設定

「Google Authenticator」を認証方法として利用するためのバンドルを追加します。

$ composer require scheb/2fa-google-authenticator

2段階認証のクライアントアプリに読み込ませるQRコードを生成するためのバンドルを追加します。

$ composer require scheb/2fa-qr-code

 

設定ファイルの調整

Google Authenticatorの利用を有効にします。

config/packages/scheb_2fa.yaml

# See the configuration reference at https://github.com/scheb/2fa/blob/master/doc/configuration.md
scheb_two_factor:
    security_tokens:
        # - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
        # If you're using guard-based authentication, you have to use this one:
        # - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
        # If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
        - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
+   google:
+       enabled: true

Userエンティティで「TwoFactorInterface」インターフェイスを実装

Userエンティティで「Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface」インターフェイスを実装します。

...   
-class User implements UserInterface, PasswordAuthenticatedUserInterface
+class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
{
...
+
+    /**
+     * @ORM\Column(name="googleAuthenticatorSecret", type="string", nullable=true)
+     */
+    private $googleAuthenticatorSecret;
+
+    public function isGoogleAuthenticatorEnabled(): bool
+    {
+        return $this->googleAuthenticatorSecret ? true : false;
+    }
+
+    public function getGoogleAuthenticatorUsername(): string
+    {
+        return $this->email;
+    }
+
+    public function getGoogleAuthenticatorSecret(): ?string
+    {
+        return $this->googleAuthenticatorSecret;
+    }
+
+    public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void
+    {
+        $this->googleAuthenticatorSecret = $googleAuthenticatorSecret;
+    }
 }

 

上記で更新したUserエンティをデータベースに反映します。

$ bin/console make:migration
$ bin/console doctrine:migrations:migrate

userテーブルに「googleAuthenticatorSecret」というカラムが追加されます。

mysql> select * from user;
+----+-------------------+-------+--------------------------------------------------------------+---------------------------+
| id | email             | roles | password                                                     | googleAuthenticatorSecret |
+----+-------------------+-------+--------------------------------------------------------------+---------------------------+
|  1 | test@maruzen-p.jp | []    | $2y$13$TKrKI4A8je1Qxn2wlw3nNeqzUeX9cYDXynW2dUDhOemD5I9jfyg3W | NULL                      |
+----+-------------------+-------+--------------------------------------------------------------+---------------------------+
1 row in set (0.00 sec)

ユーザー登録フォームの調整

ユーザー登録時に「Google Authenticator」用のシークレットコードを生成するように、ユーザー登録フォームを調整します。

src/Controller/RegistrationController.php

 use App\Entity\User;
 use App\Form\RegistrationFormType;
+use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\Google\GoogleAuthenticatorInterface;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
...
     /**
      * @Route("/register", name="app_register")
      */
-    public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
+    public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GoogleAuthenticatorInterface $googleAuthenticator): Response
     {
         $user = new User();
         $form = $this->createForm(RegistrationFormType::class, $user);
....
                     $form->get('plainPassword')->getData()
                 )
             );
+            $user->setGoogleAuthenticatorSecret($googleAuthenticator->generateSecret());

             $entityManager = $this->getDoctrine()->getManager();
             $entityManager->persist($user);


調整した登録フォームで、ユーザー(test2@maruzen-p.jp)を登録すると、次のようにDBに登録されます。
カラム「googleAuthenticatorSecret」にシークレッドコードが登録されています。

mysql> select * from user;
+----+--------------------+-------+--------------------------------------------------------------+------------------------------------------------------+
| id | email              | roles | password                                                     | googleAuthenticatorSecret                            |
+----+--------------------+-------+--------------------------------------------------------------+------------------------------------------------------+
|  1 | test@maruzen-p.jp  | []    | $2y$13$TKrKI4A8je1Qxn2wlw3nNeqzUeX9cYDXynW2dUDhOemD5I9jfyg3W | NULL                                                 |
|  2 | test2@maruzen-p.jp | []    | $2y$13$afO3xcht0zlRMNPWfVSHUOfcI5URB5cQLRt0/XsnVMZnnGrp1Ns.6 | AUHZYRZURB62QPMFFJ2A752BU44DGZYEPSAOLVQNVYN2YQBLEU4Q |
+----+--------------------+-------+--------------------------------------------------------------+------------------------------------------------------+
2 rows in set (0.00 sec)

2段階認証を試してみる

これで2段階認証が使えるようになりましたので、早速試してみましょう。

先ほど作成した「test2@maruzen-p.jp」でログインしてみます。

 

1段階目の認証に成功すると、2段階目の認証フォームが表示されます。
ここに「GoogleAuthenticator」クライアントアプリが生成する6桁のコードを打ち込むと、2段階目の認証もクリアとなります。

最後に、「GoogleAuthenticator」クライアントアプリが認証コードを生成するために必要なQRコードの作成機能を追加していきます。

QRコードの生成

「GoogleAuthenticator」クライアントアプリで読み込むQRコードを作成します。

 

QRコードを生成して表示するコントローラーを作成します。

src/Controller/QrCodeController.php

<?php

namespace App\Controller;

use Scheb\TwoFactorBundle\Security\TwoFactor\QrCode\QrCodeGenerator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class QrCodeController extends AbstractController
{
    /**
     * @Route("/qr-code", name="qr_code")
     */
    public function displayGoogleAuthenticatorQrCode(QrCodeGenerator $qrCodeGenerator)
    {
        // $qrCode is provided by the endroid/qr-code library. See the docs how to customize the look of the QR code:
        // https://github.com/endroid/qr-code
        $qrCode = $qrCodeGenerator->getGoogleAuthenticatorQrCode($this->getUser());

        return new Response($qrCode->writeString(), 200, ['Content-Type' => 'image/png']);
    }
}

 

2段階認証の途中で、上記コントローラーを表示できるように、「access_control」のpathを調整します。

     access_control:
         - { path: ^/logout, role: IS_AUTHENTICATED_ANONYMOUSLY }
+        - { path: ^/qr-code, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
         - { path: ^/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
         - { path: ^/admin, roles: ROLE_USER }

これで、2段階認証の途中で、下記URLにアクセスをすると、QRコードを表示できるようになります。
http://localhost/qr-code


 

上記を「GoogleAuthenticator」アプリで読み込むと、6桁の認証コードを生成してくれます。

この認証コードをを打ち込むと、2段階目の認証もクリアとなります。

 

今日は、2段階認証の基本的な機能の追加をご紹介させていただきました。
工夫次第で、色々な応用ができそうです。コロナ禍においてリモートワークが進む中、便利で安全なWebサービスを提供するために、みなさんもこの2段階認証を活用してみては如何でしょうか。

この記事を書いたスタッフ:ヒロシ

ヒロシです。
丸善印刷でWebのお仕事をしています。
趣味は英会話です!

人気の記事