ブログ
ニュース & ブログ
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」
「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」ページにブラウザでアクセスすると、ページの内容が表示されます。
ログインフォームを作成します。
$ 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段階認証を活用してみては如何でしょうか。