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.
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. 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!
$ 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:
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:
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:
Enter email and password of your user.
And it will be solved like...:
✨Now you can register users in your app and sign-in/out with them✨