Verify Email after Registration
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeOn some sites, after registration, you need to verify your email. You're almost definitely familiar with the process: you register, they send a special link to your email, you click that link and voilĂ ! Your email is verified. If you don't click that link, depending on the site, you might not have access to certain sections... or you may not be able to log in at all. That's what we're going to do.
When we originally ran the make:registration-form command, it asked us if we wanted to generate an email verification process. If we had said yes, it would have generated some code for us. We said no... so that we could build it by hand, learn a bit more about how it works and customize things a bit.
User.isVerified Property
But before we jump into sending the verification email, inside our User class, we need some way to track whether or not a user has verified their email. Let's add a new field for that. Run:
symfony console make:entity
Update User, add an isVerified property, boolean type, not nullable and... perfect! Head over to the class. Let's see... here we go: $isVerified:
| // ... lines 1 - 17 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 20 - 56 | |
| /** | |
| * @ORM\Column(type="boolean") | |
| */ | |
| private $isVerified; | |
| // ... lines 61 - 225 | |
| public function getIsVerified(): ?bool | |
| { | |
| return $this->isVerified; | |
| } | |
| public function setIsVerified(bool $isVerified): self | |
| { | |
| $this->isVerified = $isVerified; | |
| return $this; | |
| } | |
| } |
Let's default this to false:
| // ... lines 1 - 17 | |
| class User implements UserInterface, PasswordAuthenticatedUserInterface | |
| { | |
| // ... lines 20 - 59 | |
| private $isVerified = false; | |
| // ... lines 61 - 236 | |
| } |
Ok, time for the migration:
symfony console make:migration
Go check that out and... awesome. It looks exactly like we expect:
| // ... lines 1 - 4 | |
| namespace DoctrineMigrations; | |
| use Doctrine\DBAL\Schema\Schema; | |
| use Doctrine\Migrations\AbstractMigration; | |
| /** | |
| * Auto-generated Migration: Please modify to your needs! | |
| */ | |
| final class Version20211012235912 extends AbstractMigration | |
| { | |
| public function getDescription(): string | |
| { | |
| return ''; | |
| } | |
| public function up(Schema $schema): void | |
| { | |
| // this up() migration is auto-generated, please modify it to your needs | |
| $this->addSql('ALTER TABLE user ADD is_verified TINYINT(1) NOT NULL'); | |
| } | |
| public function down(Schema $schema): void | |
| { | |
| // this down() migration is auto-generated, please modify it to your needs | |
| $this->addSql('ALTER TABLE user DROP is_verified'); | |
| } | |
| } |
Run it!
symfony console doctrine:migrations:migrate
Beautiful! Let's do one more thing related to the database. Inside of src/Factory/UserFactory.php, to make life simpler, set $isVerified to true:
| // ... lines 1 - 29 | |
| final class UserFactory extends ModelFactory | |
| { | |
| // ... lines 32 - 40 | |
| protected function getDefaults(): array | |
| { | |
| return [ | |
| // ... lines 44 - 46 | |
| 'isVerified' => true, | |
| ]; | |
| } | |
| // ... lines 50 - 68 | |
| } |
So, by default, users in our fixtures will be verified. But I won't worry about reloading my fixtures quite yet.
Hello VerifyEmailBundle!
Okay: now let's add the email confirmation system! How? By leveraging a bundle! At your terminal, run:
composer require "symfonycasts/verify-email-bundle:1.11.0"
Hey, I know them! This bundle gives us a couple of services that will help generate a signed URL that we will include in the email and then validate that signed URL when the user clicks it. To get this working, open up RegistrationController. We already have our working register() method. Now we need one other method. Add public function verifyUserEmail(). Above this, give it a route: @Route("/verify") with name="app_verify_email":
| // ... lines 1 - 14 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 17 - 53 | |
| /** | |
| * @Route("/verify", name="app_verify_email") | |
| */ | |
| public function verifyUserEmail(): Response | |
| { | |
| // TODO | |
| } | |
| } |
When the user clicks the "confirm email" link in the email that we send them, this is the route and controller that link will take them to. I'm going to leave it empty for now. But eventually, its job will be to validate the signed URL, which will prove that the user did click on the exact link that we sent them.
Sending the Confirmation Email
Up in the register() action, here is where we need to send that email. As I mentioned earlier, you can do different things on your site based on whether or not the user's email is verified. In our site, we are going to completely prevent the user from logging in until their email is verified. So I'm going to remove the $userAuthenticator stuff:
| // ... lines 1 - 14 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 17 - 19 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, FormLoginAuthenticator $formLoginAuthenticator): Response | |
| { | |
| // ... lines 22 - 25 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 27 - 39 | |
| $userAuthenticator->authenticateUser( | |
| $user, | |
| $formLoginAuthenticator, | |
| $request | |
| ); | |
| return $this->redirectToRoute('app_homepage'); | |
| } | |
| // ... lines 48 - 51 | |
| } | |
| // ... lines 53 - 60 | |
| } |
And replace that with the original redirect to app_homepage:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 47 | |
| return $this->redirectToRoute('app_homepage'); | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
Up top, we can remove some arguments.
Cool. Now we need to generate the signed email confirmation link and send it to the user. To do that, autowire a new service type-hinted with VerifyEmailHelperInterface. Call it $verifyEmailHelper:
| // ... lines 1 - 11 | |
| use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface; | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
Below, after we save the user, let's generate that signed URL. This... looks a little weird at first. Say $signatureComponents equals $verifyEmailHelper->generateSignature():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 35 | |
| $entityManager->flush(); | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| // ... lines 39 - 42 | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
The first argument is the route name to the verification route. For us, that will be app_verify_email:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 55 | |
| /** | |
| * @Route("/verify", name="app_verify_email") | |
| */ | |
| public function verifyUserEmail(): Response | |
| { | |
| // ... line 61 | |
| } | |
| } |
So I'll put that here. Then the user's id - $user->getId() - and the user's email, $user->getEmail():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| // ... line 42 | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
These are both used to "sign" the URL, which will help prove that this user did click the link from the email we sent them:
Verifying the Email without Being Logged In
But now we have a decision point. There are two different ways to use the VerifyEmailBundle. The first one is where, when the user clicks this email confirmation link, you expect them to be logged in. In this situation, down in verifyUserEmail(), we can use $this->getUser() to figure out who is trying to verify their email and use that to help validate the signed URL.
The other way, which is the way that we're going to use, is to allow the user to not be logged in when they click the confirmation link in their email. With this mode, we need to pass an array as the final argument to include the user id:
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| ['id' => $user->getId()] | |
| ); | |
| // ... lines 44 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
The whole point of this generateSignature() method is to generate a signed URL. And thanks to this last argument, that URL will now contain an id query parameter... which we can use down in the verifyUserEmail() method to query for the User. We'll see that soon.
At this point, in a real app, you would take this $signatureComponents thing, pass it into an email template, use it to render the link and then send the email. But this is not a tutorial about sending emails - though we do have that tutorial. So I'm going to take a shortcut. Instead of sending an email, say $this->addFlash('success') with a little message that says, "Confirm your email at:" and then the signed URL. You can generate that by saying $signatureComponents->getSignedUrl():
| // ... lines 1 - 13 | |
| class RegistrationController extends AbstractController | |
| { | |
| // ... lines 16 - 18 | |
| public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, VerifyEmailHelperInterface $verifyEmailHelper): Response | |
| { | |
| // ... lines 21 - 24 | |
| if ($form->isSubmitted() && $form->isValid()) { | |
| // ... lines 26 - 37 | |
| $signatureComponents = $verifyEmailHelper->generateSignature( | |
| 'app_verify_email', | |
| $user->getId(), | |
| $user->getEmail(), | |
| ['id' => $user->getId()] | |
| ); | |
| // TODO: in a real app, send this as an email! | |
| $this->addFlash('success', 'Confirm your email at: '.$signatureComponents->getSignedUrl()); | |
| // ... lines 47 - 48 | |
| } | |
| // ... lines 50 - 53 | |
| } | |
| // ... lines 55 - 62 | |
| } |
We haven't talked about flash messages. They're basically temporary messages that you can put into the session... then render them one time. I put this message into a success category. Thanks to this, over in templates/base.html.twig, right after the navigation - so it's on top of the page - we can render any success flash messages. Add for flash in app.flashes() and then look up that key success. Inside, add div with alert, alert-success and render the message:
| <html> | |
| // ... lines 3 - 14 | |
| <body | |
| // ... lines 16 - 81 | |
| {% for flash in app.flashes('success') %} | |
| <div class="alert alert-success">{{ flash }}</div> | |
| {% endfor %} | |
| {% block body %}{% endblock %} | |
| // ... lines 87 - 89 | |
| </body> | |
| </html> |
This flash stuff has nothing to do with email confirmation... it's just a feature of Symfony that's most commonly used when you're handling forms. But it's a nice shortcut to help us test this.
Next: let's... do that! Test out our registration form and see what this signed URL looks like. Then we'll fill in the logic to verify that URL and confirm our user.
11 Comments
Hey there!
I'm glad it was useful!
> it seems there isn't functionality regarding resending the verification email after expiration. There seems some open issues (#35, #50) for this enhancement in the repo, but there hasn't been an update on those yet. Do you think such functionality will be added to the bundle in the near future or do we need to implement it ourselves?
I'm not sure. jrushlow helps me maintain that bundle, but we're all pretty busy. The ideal situation would be if someone from the community could add it. But, we have an internal OSS issue tracker, and I'll add this to it :). I agree that it really IS something the bundle needs.
Cheers!
I was needed to "verify email" functionality and at 6:27 on the video where Ryan has made a shortcut of FLASH message instead of sending email I was a little upset.
And I really began to prepare for learning the "Symfony Mailer: Love Sending Emails Again" course.
But when I was exploring the other courses, I found a brilliant =)
There is an extension for this functionality with exactly this code in "What's new in Symfony6" course.
Here is the link with video of sending emails: https://symfonycasts.com/screencast/symfony6-upgrade/docker
Maybe it will be helpful if you need a quick explanation how to send emails.
Thank you, Ryan. Again... =)
Hey @Magnoom Thanks for sharing it with others. Cheers!
I am trying to use an actual email verification while registering. To make things simple, I have chosen to use a verification email right in the registration form maker.
In the the register controller, in the register function, I have a call to
$this->emailVerifier->sendEmailConfirmation. ThesendEmailConfirmationdoes use$this->mailer->send($email);, which I guess, sends the email.As the email sending provider, I have chose Brevo and downloded the corresponding bundle. In the
.envfile. I have setMAILER_DSNtobrevo+api://KEY@defaultwhere I have replace KEY by my Brevo API key. I have not received any email and, as a matter of fact, the created user has ais_verifiedkey set to 0.Should I modify
@defaultin the MAILER_DSN entry?At this stage, I am rather confused. I have had a look at the Sfcast course on the symfony mailer, bur it seems that in the course an actual email is never sent (it uses a bogus email service). Since I do not get the confirmation email for real, I cannot check if the verification link works.
TIA,
François
Hey Francois,
Make sure you send emails sync and not async. If you have async emails sending - the email messages most probably hit your message queue, and in this case you would need to run the Messenger worker to consume those messages from the queue and actually send them.
Also, for email debugging purposes, you can leverage Mailtrap service... or some software that you can spin locally like Mailcatcher or MailHog. With this you would not need to use paid services like Brevo to actually send emails for debugging.
Cheers!
Thanks!
Things worked after setting the following line in the
messenger.yamlfile:BTW, at Brevo, the free plan lets you send 300 emails per day, which lets you do some testing without credit card
Have a good day!
Hey Francois,
Yeah, making that sync should simplify things locally. Instead of doing it for specific messages, you can do it globally by setting the
MESSENGER_TRANSPORT_DSN=sync://- then all your async messages will work sync - easier for debugging locally :)And yeah, a free plan with some limitations is useful too :)
Cheers!
Can you please tell me how to debug and change as example token_expiration ?
Hey Mepcuk@
You want to change the lifetime of the token expiration? I realize that we don't document this! At your command line, run:
That should dump the example config that's allowed for this bundle. You'll find that you can (in any YAML file in config/packages - so just create a new one) say:
Let me know if this is what you were looking for :).
Cheers!
Hi, I have an issue with this function : verifyUserEmail.
I don't know how write this code:
$user = $userRepository->find();
$request->query->get('id');
OR
$user = $userRepository->find($request->query->get('id'));
Can someone help me please ?
Louis G.
Hey Louis Gellez!
The second is correct:
The
$request->query->get('id')returns the string ?id" query parameter (e.g. if the URL ended in ?id=5, this would return "5"). Then this is passed to the find() method, which finds the User with that id. You'll find this exact code in the "finish" directory of the code download. And once we record this chapter (actually I moved this content to the NEXT chapter today - https://symfonycasts.com/screencast/symfony-security/verify-signed-url ) then you will be able to see it clearly in the code blocks.Cheers!
"Houston: no signs of life"
Start the conversation!