Symfony 6 User Authentication

created
( modified )
@nabbisen

Summary

Symfony is one of PHP web frameworks. It is my favorite one, because it is clearly classified, functional and robust.

It is designed with security in mind, is accompanied with useful helpers like MakerBundle, and also provides great official documentation.

eyecatch

This post shows how to implement user management and authentication with Symfony.

Environment

Reference


Tutorial

Overview

Thanks to symfony/security-bundle, we don’t have to define user entity as PHP code or database schema from the beginning, for the bundle(s) brings them, which is, of course, able to be customized. All we have to do is run few commands, configure in some ways and write some code on view template and controller.

Here we go.


1. Preparation

1-1. Symfony project

If you have no Symfony project, create your project directory and enter it. Then run:

$ php composer create-project \
      symfony/website-skeleton "."

$ php composer require \
      symfony/orm-pack \
      symfony/serializer
$ php composer require --dev \
      symfony/maker-bundle

Well, symfony/orm-pack comes with doctrine/doctrine-bundle. You may need to edit .env and modify DATABASE_URL to connect to your database server or file.

1-2. Install Symfony security bundle

Ready? Let’s build the foundation:

$ composer require symfony/security-bundle

That’s it :)

2. User management system

2-1. Create user storage

From now on, starts the process to create the user management system of your own. This command line generates user entity and the surroundings resource.

$ php bin/console make:user

The output was below. Here, I chose the default at all. Actually, it’s up to you.

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

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

 Enter a property name that will be the unique "display" name for the user (e.g. email, username, uuid) [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]:
 > 

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

           
  Success! 
           

 Next Steps:
   - Review your new App\Entity\User class.
   - Use make:entity to add more fields to your User entity and then run make:migration.
   - Create a way to authenticate! See https://symfony.com/doc/current/security.html

Update the database:

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

2-2. Create user registration form

Next, create form for users to register. Run:

$ php bin/console make:registration-form

The output and the questions I got were below. I tended to follow the recommendations here:

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

Well, when you choose “yes” at the next question, you also have to take another installation. (I’ll show it in the next “Optional” section.)

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

                                                                                                                        
 [WARNING] We're missing some important components. Don't forget to install these after you're finished.                
                                                                                                                        
           composer require symfonycasts/verify-email-bundle                                                            

The rest was:

 By default, users are required to be authenticated when they click the verification link that is emailed to them.
 This prevents the user from registering on their laptop, then clicking the link on their phone, without
 having to log in. To allow multi device email verification, we can embed a user id in the verification link.

 Would you like to include the user id in the verification link to allow anonymous email verification? (yes/no) [no]:
 >    

 What email address will be used to send registration confirmations? (e.g. [email protected]):
 > mailer@(your-domain)

 What "name" should be associated with that email address? (e.g. Acme Mail Bot):
 > Acme Mail Bot

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

 ! [NOTE] No Guard authenticators found - so your user won't be automatically authenticated after registering.          

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

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

           
  Success! 
           

 Next:
 1) Install some missing packages:
      composer require symfonycasts/verify-email-bundle
 2) In RegistrationController::verifyUserEmail():
    * Customize the last redirectToRoute() after a successful email verification.
    * Make sure you're rendering success flash messages or change the $this->addFlash() line.
 3) Review and customize the form, controller, and templates as needed.
 4) Run "php bin/console make:migration" to generate a migration for the newly added User::isVerified property.

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

2-3. (Optional) Implement verifier via email

Which did you choose there?

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

“yes”? Me, too. In that case, we need to install the additional bundle which is required. It’s none of tough work!

$ composer require symfonycasts/verify-email-bundle

The output was:

Info from https://repo.packagist.org: #StandWithUkraine
Using version ^1.12 for symfonycasts/verify-email-bundle
./composer.json has been updated
Running composer update symfonycasts/verify-email-bundle
Loading composer repositories with package information
Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals
  - Locking symfonycasts/verify-email-bundle (v1.12.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Downloading symfonycasts/verify-email-bundle (v1.12.0)
  - Installing symfonycasts/verify-email-bundle (v1.12.0): Extracting archive
Generating optimized autoload files
109 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

Symfony operations: 1 recipe (c63cd854aac79ffae347ea7cceaa2e44)
  - Configuring symfonycasts/verify-email-bundle (>=v1.12.0): From auto-generated recipe
Executing script cache:clear [OK]
Executing script assets:install public [OK]
              
 What's next? 
              

Some files have been created and/or updated to configure your new packages.
Please review, edit and commit them: these files are yours.

No security vulnerability advisories found

Then update the database:

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

Edit .env. For example (for testing):

  ###> symfony/mailer ###
- # MAILER_DSN=null://null
+ MAILER_DSN=null://null
  ###< symfony/mailer ###

That’s it!

2-4 Let’s play: Your registration form in browser

Go to https://(your-domain)/register. You will see:

registration form

The form and the backend system are actually ready. Try yourself!! The user you enter will be registered in User table in your database. Also, when you use email verifier and set valid MAILER_DSN, you will receive an email which contains link to verify.

Now user management system is ready. Wonderful. Next, let’s build up user authentication system.

3. User authentication system

3-1. Create user sign-in form and sign-out route

Create Symfony controller for authentication with Maker(Bundle):

$ php bin/console make:controller Auth

The output was:

 created: src/Controller/AuthController.php
 created: templates/auth/index.html.twig

           
  Success! 
           

 Next: Open your new controller class and add some pages!

Create view for sign-in. First, rename the default template:

$ mv templates/auth/index.html.twig templates/auth/sign-in.html.twig

Then edit it (templates/auth/sign-in.html.twig):

  {% block body %}
- # all !!
+ {% if error %}
+     <div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
+ {% endif %}
+ 
+ <form action="{{ path('app_sign_in') }}" method="post">
+     <label for="username">Email:</label>
+     <input type="text" id="username" name="_username" value="{{ last_username }}"/>
+ 
+     <label for="password">Password:</label>
+     <input type="password" id="password" name="_password"/>
+ 
+     {# If you want to control the URL the user is redirected to on success
+     <input type="hidden" name="_target_path" value="/account"/> #}
+ 
+     <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
+ 
+     <button type="submit">login</button>
+ </form>
  {% endblock %}

Next, configure config/packages/security.yaml to activate routes of sign-in via form and sign-out (which are defined next):

  security:
      # ...
      firewalls:
          # ...
          main:
              # ...
+             form_login:
+                 login_path: app_sign_in
+                 check_path: app_sign_in
+                 enable_csrf: true
+             logout:
+                 path: app_sign_out

Finally, edit src/AuthController.php to define routes for sign-in and sign-out:

+ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  # ...
  class AuthController extends AbstractController
  {
-     #[Route('/auth', name: 'app_auth')]
-     public function index(): Response
-     {
-         return $this->render('auth/index.html.twig', [
-             'controller_name' => 'AuthController',
-         ]);
-     }
+     #[Route('/sign-in', name: 'app_sign_in')]
+     public function app_sign_in(AuthenticationUtils $authenticationUtils): Response
+     {
+         // get the login error if there is one
+         $error = $authenticationUtils->getLastAuthenticationError();
+ 
+         // last username entered by the user
+         $lastUsername = $authenticationUtils->getLastUsername();
+ 
+         return $this->render('auth/sign-in.html.twig', [
+             'last_username' => $lastUsername,
+             'error'         => $error,
+         ]);
+     }
+ 
+     #[Route('/sign-out', name: 'app_sign_out')]
+     public function app_sign_out(): Response
+     {
+         // controller can be blank: it will never be called!
+         throw new \Exception('Don\'t forget to activate logout in security.yaml');
+     }
  }

3-2. Let’s play: User authentication and access control

Let’s check if user authentication works properly. First, create a page where anonymous user can’t view by editting config/packages/security.yaml. Additionally, Comment out the form_login: section in order to deactivate redirection to sign-in form:

  security:
      # ...
      firewalls:
          # ...
-         form_login:
-             login_path: app_sign_in
-             check_path: app_sign_in
-             enable_csrf: true
+         # form_login:
+             # login_path: app_sign_in
+             # check_path: app_sign_in
+             # enable_csrf: true
      # ...
      access_control:
          # ...
+         - { path: ^/app, roles: ROLE_USER }

Open https://your-domain)/app in browser. You will be unable to access the page and see the error:

access denied

OK. Access control works. Then, restore form_login::

  security:
      # ...
      firewalls:
          # ...
-         # form_login:
-             # login_path: app_sign_in
-             # check_path: app_sign_in
-             # enable_csrf: true
+         form_login:
+             login_path: app_sign_in
+             check_path: app_sign_in
+             enable_csrf: true

Open, again. You will be redirected to the sign-in page:

redirection to sign-in form

Enter email and password of your user. And it will be solved like…:

sign-in successful

✨Now you can register users in your app and sign-in/out with them✨


Comments or feedbacks are welcomed and appreciated.