Symfony 6: User Authentication
Tags: php, symfony, security, authentication
Published: 2022-11-10 14:21:52

## Summary

[Symfony](https://symfony.com/) is one of [PHP](https://www.php.net/) 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](https://symfony.com/doc/current/bundles/SymfonyMakerBundle/index.html), and also provides great official [documentation](https://symfony.com/doc/current/index.html).

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

### Environment

- [Alpine Linux](https://www.alpinelinux.org/) [3.16](https://wiki.alpinelinux.org/wiki/Release_Notes_for_Alpine_3.16.0) (on [Docker](https://www.docker.com/))
- PHP [8.1](https://www.php.net/releases/8.1/)
- Symfony [6.1](https://symfony.com/releases/6.1)
- [MariaDB](https://mariadb.org/) [10.9](https://mariadb.com/kb/en/mariadb-server-109/)

### Reference

- [Symfony 6.1 Security](https://symfony.com/doc/6.1/security.html)

---

## Tutorial

### Overview

Thanks to [symfony/security-bundle](https://symfony.com/components/Security%20Bundle), 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:

```console
$ 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](https://symfony.com/components/ORM%20Pack) comes with [doctrine/doctrine-bundle](https://symfony.com/bundles/DoctrineBundle/current/index.html). 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:

```console
$ 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.

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

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

```console
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:

```console
$ 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:

```console
$ php bin/console make:registration-form
```

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

```console
 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.)

```console
 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:

```console
 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. mailer@your-domain.com):
 > 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!

```console
$ composer require symfonycasts/verify-email-bundle
```

The output was:

```console
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:

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

Edit `.env`. For example (for testing):

```diff
  ###> 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yppf2af21m60mlyiersb.png)

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

```console
$ php bin/console make:controller Auth
```

The output was:

```console
 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:

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

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

```diff
  {% 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):

```diff
  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:

```diff
+ 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:

```diff
  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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8rcz8nsc3dvj4g0plw2z.png)

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

```diff
  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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hnk67cna3uxdenv07d4l.png)

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

![sign-in successful](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4bhze4gqldq2z9r8cenw.png)

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