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 SubscribeHey! You've made it through almost this entire tutorial! Nice work! I have just a few more tricks to show you before we're done - and they're good ones!
First, I want to create a registration form. Find your code and open SecurityController
. In addition to login
and logout
, add a new public function register()
:
... lines 1 - 8 | |
class SecurityController extends AbstractController | |
{ | |
... lines 11 - 38 | |
public function register() | |
{ | |
... line 41 | |
} | |
} |
Give it a route - /register
and a name: app_register
:
... lines 1 - 8 | |
class SecurityController extends AbstractController | |
{ | |
... lines 11 - 35 | |
/** | |
* @Route("/register", name="app_register") | |
*/ | |
public function register() | |
{ | |
... line 41 | |
} | |
} |
Here's the interesting thing about registration. It has nothing to do with security! Think about it. What is registration? It's just a form that creates a new record in the User
table. That's it! That's just database stuff.
So then... why are we even talking about this in a security tutorial? Well... to create the best user experience, there will be just a little bit of security right at the end. Because, after registration, I want to instantly authenticate the new user.
More on that later. Right now, render a template: $this->render('security/register.html.twig')
:
... lines 1 - 8 | |
class SecurityController extends AbstractController | |
{ | |
... lines 11 - 35 | |
/** | |
* @Route("/register", name="app_register") | |
*/ | |
public function register() | |
{ | |
return $this->render('security/register.html.twig'); | |
} | |
} |
Then... I'll cheat: in security/
, copy the login.html.twig
template, paste and call it register.html.twig
:
{% extends 'base.html.twig' %} | |
{% block title %}Login!{% endblock %} | |
{% block stylesheets %} | |
{{ parent() }} | |
<link rel="stylesheet" href="{{ asset('css/login.css') }}"> | |
{% endblock %} | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
<form class="form-signin" method="post"> | |
{% if error %} | |
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> | |
{% endif %} | |
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> | |
<label for="inputEmail" class="sr-only">Email address</label> | |
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> | |
<label for="inputPassword" class="sr-only">Password</label> | |
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> | |
<input type="hidden" name="_csrf_token" | |
value="{{ csrf_token('authenticate') }}" | |
> | |
<div class="checkbox mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me"> Remember me | |
</label> | |
</div> | |
<button class="btn btn-lg btn-primary btn-block" type="submit"> | |
Sign in | |
</button> | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Let's see: change the title, delete the authentication error stuff and I am going to add a little comment here that says that we should replace this with a Symfony form later:
... lines 1 - 2 | |
{% block title %}Register!{% endblock %} | |
... lines 4 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
{# todo - replace with a Symfony form! #} | |
<form class="form-signin" method="post"> | |
... lines 17 - 30 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
We haven't talked about the form system yet, so I don't want to use it here. But, normally, I would use the form system because it handles validation and automatically adds CSRF protection.
But, to show off how to manually authenticate a user after registration, this HTML form will work beautifully. Change the h1
, remove the value=
on the email
field so that it always starts blank and take out the CSRF token:
... lines 1 - 2 | |
{% block title %}Register!{% endblock %} | |
... lines 4 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
{# todo - replace with a Symfony form! #} | |
<form class="form-signin" method="post"> | |
<h1 class="h3 mb-3 font-weight-normal">Register</h1> | |
<label for="inputEmail" class="sr-only">Email address</label> | |
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> | |
<label for="inputPassword" class="sr-only">Password</label> | |
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> | |
... lines 22 - 30 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
We do need CSRF protection on this form... but I'll skip it for now, because we'll refactor this into a Symfony form in a future tutorial.
And finally, hijack the "remember me" checkbox and turn it into a terms box. We'll say:
Agree to terms I for sure read
... lines 1 - 2 | |
{% block title %}Register!{% endblock %} | |
... lines 4 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
{# todo - replace with a Symfony form! #} | |
<form class="form-signin" method="post"> | |
<h1 class="h3 mb-3 font-weight-normal">Register</h1> | |
<label for="inputEmail" class="sr-only">Email address</label> | |
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> | |
<label for="inputPassword" class="sr-only">Password</label> | |
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> | |
<div class="checkbox mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me" required> Agree to terms I for sure read | |
</label> | |
</div> | |
... lines 28 - 30 | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Oh, and update the button: Register:
... lines 1 - 2 | |
{% block title %}Register!{% endblock %} | |
... lines 4 - 10 | |
{% block body %} | |
<div class="container"> | |
<div class="row"> | |
<div class="col-sm-12"> | |
{# todo - replace with a Symfony form! #} | |
<form class="form-signin" method="post"> | |
<h1 class="h3 mb-3 font-weight-normal">Register</h1> | |
<label for="inputEmail" class="sr-only">Email address</label> | |
<input type="email" name="email" id="inputEmail" class="form-control" placeholder="Email address" required autofocus> | |
<label for="inputPassword" class="sr-only">Password</label> | |
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required> | |
<div class="checkbox mb-3"> | |
<label> | |
<input type="checkbox" name="_remember_me" required> Agree to terms I for sure read | |
</label> | |
</div> | |
<button class="btn btn-lg btn-primary btn-block" type="submit"> | |
Register | |
</button> | |
</form> | |
</div> | |
</div> | |
</div> | |
{% endblock %} |
Let's see how it looks! Move over, go to /register
and... got it! Logout, then move back over and open up base.html.twig
. Scroll down just a little bit to find the "Login" link. Let's create a second link that points to the new app_register
route. Say, "Register":
<html lang="en"> | |
... lines 3 - 15 | |
<body> | |
... lines 17 - 22 | |
<nav class="navbar navbar-expand-lg navbar-dark navbar-bg mb-5"> | |
... lines 24 - 27 | |
<div class="collapse navbar-collapse" id="navbarNavDropdown"> | |
... lines 29 - 40 | |
<ul class="navbar-nav ml-auto"> | |
{% if is_granted('ROLE_USER') %} | |
... lines 43 - 58 | |
<li class="nav-item"> | |
<a style="color: #fff;" class="nav-link" href="{{ path('app_register') }}">Register</a> | |
</li> | |
{% endif %} | |
</ul> | |
</div> | |
</nav> | |
... lines 66 - 83 | |
</body> | |
</html> |
Move back and check it out. Not bad!
Just like with the login
form, because there is no action=
on the form, this will submit right back to the same URL. But, unlike login, because this is just a normal page, we are going to handle that submit logic right inside of the controller.
First, get the Request object by adding an argument with the Request
type hint: the one from HttpFoundation. Below, I'm going to add another reminder to use the Symfony form & validation system later:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
... lines 45 - 55 | |
} | |
} |
Then, to only process the data when the form is being submitted, add if ($request->isMethod('POST'))
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 52 | |
} | |
... lines 54 - 55 | |
} | |
} |
Inside... our job is simple! Registration is nothing more than a mechanism to create a new User
object. So $user = new User()
. Then set some data on it: $user->setEmail($request->request->get('email'))
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
$user = new User(); | |
$user->setEmail($request->request->get('email')); | |
... lines 48 - 52 | |
} | |
... lines 54 - 55 | |
} | |
} |
Remember $request->request
is the way that you get $_POST
data. And, the names of the fields on our form are name="email"
and name="password"
. But before we handle the password, add $user->setFirstName()
. This field is required in the database... but, we don't actually have that field on the form. Just use Mystery
for now:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
$user = new User(); | |
$user->setEmail($request->request->get('email')); | |
$user->setFirstName('Mystery'); | |
... lines 49 - 52 | |
} | |
... lines 54 - 55 | |
} | |
} |
In a real app, I would either add this field to the registration form, or make it nullable
in the database, so it's optional.
Finally, let's set the password. But... of course! We are never ever, ever, ever going to save the plain password. We need to encode it. We already did this inside of UserFixture
:
... lines 1 - 7 | |
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | |
class UserFixture extends BaseFixture | |
{ | |
private $passwordEncoder; | |
public function __construct(UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
$this->passwordEncoder = $passwordEncoder; | |
} | |
protected function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(10, 'main_users', function($i) use ($manager) { | |
... lines 22 - 29 | |
$user->setPassword($this->passwordEncoder->encodePassword( | |
$user, | |
'engage' | |
)); | |
... lines 34 - 40 | |
}); | |
$this->createMany(3, 'admin_users', function($i) { | |
... lines 44 - 48 | |
$user->setPassword($this->passwordEncoder->encodePassword( | |
$user, | |
'engage' | |
)); | |
... lines 53 - 54 | |
}); | |
... lines 56 - 57 | |
} | |
} |
Ah yes, the key was the UserPasswordEncoderInterface
service. In our controller, add another argument: UserPasswordEncoderInterface
$passwordEncoder
:
... lines 1 - 8 | |
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; | |
... lines 10 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
... lines 44 - 55 | |
} | |
} |
Below, we can say $passwordEncoder->encodePassword()
. This needs the User
object and the plain password that was just submitted: $request->request->get('password')
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 48 | |
$user->setPassword($passwordEncoder->encodePassword( | |
$user, | |
$request->request->get('password') | |
)); | |
} | |
... lines 54 - 55 | |
} | |
} |
We are ready to save! Get the entity manager with $em = $this->getDoctrine()->getManager()
. Then, $em->persist($user)
and $em->flush()
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 48 | |
$user->setPassword($passwordEncoder->encodePassword( | |
$user, | |
$request->request->get('password') | |
)); | |
$em = $this->getDoctrine()->getManager(); | |
$em->persist($user); | |
$em->flush(); | |
... lines 57 - 58 | |
} | |
... lines 60 - 61 | |
} | |
} |
All delightfully boring code. This looks a lot like what we're doing in our fixtures.
Finally, after any successful form submit, we always redirect. Use return $this->redirectToRoute()
. This is the shortcut method that we were looking at earlier. Redirect to the account page: app_account
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 48 | |
$user->setPassword($passwordEncoder->encodePassword( | |
$user, | |
$request->request->get('password') | |
)); | |
$em = $this->getDoctrine()->getManager(); | |
$em->persist($user); | |
$em->flush(); | |
return $this->redirectToRoute('app_account'); | |
} | |
... lines 60 - 61 | |
} | |
} |
Awesome! Let's give this thing a spin! I'll register as ryan@symfonycasts.com
, password engage
. Agree to the terms that I for sure read and... Register! Bah! That smells like a Ryan mistake! Yep! Use $this->getDoctrine()->getManager()
:
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 53 | |
$em = $this->getDoctrine()->getManager(); | |
... lines 55 - 58 | |
} | |
... lines 60 - 61 | |
} | |
} |
That's what I meant to do.
Move over and try this again: ryan@symfonycasts.com
, password engage
, agree to the terms that I read and... Register!
Um... what? We're on the login form? What happened? First, according to the web debug toolbar, we are still anonymous. That makes sense: we registered, but we did not login. After registration, we were redirected to /account
...
... lines 1 - 11 | |
class SecurityController extends AbstractController | |
{ | |
... lines 14 - 41 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 46 - 57 | |
return $this->redirectToRoute('app_account'); | |
} | |
... lines 60 - 61 | |
} | |
} |
But because we are not logged in, that sent us here.
This is not the flow that I want my users to experience. Nope, as soon as the user registers, I want to log them in automatically.
Oh, and there's also another problem. Open LoginFormAuthenticator
and find onAuthenticationSuccess()
:
... lines 1 - 19 | |
class LoginFormAuthenticator extends AbstractFormLoginAuthenticator | |
{ | |
... lines 22 - 74 | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) | |
{ | |
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { | |
return new RedirectResponse($targetPath); | |
} | |
return new RedirectResponse($this->router->generate('app_homepage')); | |
} | |
... lines 83 - 87 | |
} |
We added some extra code here to make sure that if the user went to, for example, /admin/comment
as an anonymous user, then, after they log in, they would be sent back to /admin/comment
.
And... hey! I want that same behavior for my registration form! Imagine that you're building a store. As an anonymous user, I add some things to my cart and finally go to /checkout
. But because /checkout
requires me to be logged in, I'm sent to the login form. And because I don't have an account yet, I instead click to register and fill out that form. After submitting, where should I be taken to? That's easy! I should definitely be taken back to /checkout
so I can continue what I was doing!
These two problems - the fact that we want to automatically authenticate the user after registration and redirect them intelligently - can be solved at the same time! After we save the User
to the database, we're basically going to tell Symfony to use our LoginFormAuthenticator
class to authenticate the user and redirect by using its onAuthenticationSuccess()
method.
Check it out: add two arguments to our controller. First, a service called GuardAuthenticatorHandler $guardHandler
. Second, the authenticator that you want to authenticate through: LoginFormAuthenticator $formAuthenticator
:
... lines 1 - 5 | |
use App\Security\LoginFormAuthenticator; | |
... lines 7 - 10 | |
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; | |
... lines 12 - 13 | |
class SecurityController extends AbstractController | |
{ | |
... lines 16 - 43 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
... lines 46 - 68 | |
} | |
} |
Once we have those two things, instead of redirecting to a normal route use return $guardHandler->authenticateUserAndHandleSuccess()
:
... lines 1 - 13 | |
class SecurityController extends AbstractController | |
{ | |
... lines 16 - 43 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 48 - 59 | |
return $guardHandler->authenticateUserAndHandleSuccess( | |
... lines 61 - 64 | |
); | |
} | |
... lines 67 - 68 | |
} | |
} |
This needs a few arguments: the $user
that's being logged in, the $request
object, the authenticator - $formAuthenticator
and the "provider key". That's just the name of your firewall: main
:
... lines 1 - 13 | |
class SecurityController extends AbstractController | |
{ | |
... lines 16 - 43 | |
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, LoginFormAuthenticator $formAuthenticator) | |
{ | |
// TODO - use Symfony forms & validation | |
if ($request->isMethod('POST')) { | |
... lines 48 - 59 | |
return $guardHandler->authenticateUserAndHandleSuccess( | |
$user, | |
$request, | |
$formAuthenticator, | |
'main' | |
); | |
} | |
... lines 67 - 68 | |
} | |
} |
Cool! Let's try it! Click back to register. This time, make sure that you register as a different user, password engage
, agree to the terms, submit and... nice! We're authenticated and sent to the correct place.
Next - we're going to start talking about a very important and very fun feature called "voters". Voters are the way to make more complex access decisions, like, determining that a User can edit this Article because they are its author, but not an Article created by someone else.
Yo GDIBass!
Ha! Great question and... I have no answer :D. I keep going "back and forth" between what I like better: $this->getDoctrine()->getManager()
or EntityManagerInterface $e
. There's no technical reason for it.
But, you're right that, on a GET request, type-hinting it would technically be wasteful. I don't usually worry about these things - it's kind of a micro-optinmization.. and we probably are using the entity manager somewhere else anyways. However, it's a very good detail to notice :).
Cheers!
Hi
Thx a lot for this great cast! Everything works perfect. If a user make a request for resetting his password, it will stored in the resetPasswordRequest Entity. But if the user tries to request another email (how it's written in the reset-password/check-email: If you don't receive an email please check your spam folder or try again(link to reset/password.) no new email will be sent. A new email will be only sent, when the expiresAt entry is run up. How can I reach that, the user can request a second email?
Thx in advance!
Hey Michael,
I suppose resetPasswordRequest is something from *your* project, right? I don't see any mentions of it in this course :) So I guess it's something you write yourself. If so, you need to track the time when the password reset was requested, and before deciding send the email or not - you will need to do some checks. For this, for example, you would need to fetch your latest resetPasswordRequest entity for the given email. If there's no entity - definitely send an email, but if there's an entity already - check when the password reset was requested in that entity, and if it was requested long enough and we're OK to send another email the user now - send it. If not, i.e. it's too early for sending a new email again - just skip email sending. Usually, for users it's the same message in both cases, something like "the email was sent - check your inbox or spam", but behind the scene we do not send the 2nd email if it's too short. The time after which you're ok to send another email is up to you of course. Well, the business logic is also totally up to you, you can do whatever logic you want. You may even send the email every time the user creates a new request, but it may cause much more emails sending, and if you're paying for every email - it's probably not a good idea for you as it will be much expensive.
Anyway, I hope this helps!
Cheers!
Hey Victor
Sorry, I thought it was in this course, but I installed reset-password-bundle (https://github.com/symfonyc.... Thanks a lot for your great explanations! I understand your ideas. I think it's the logic of the bundle, that a second request is only possible, after the lifetime of the token has ended? So I can hook there and create my own logic.
Cheers
Michael
Hey Michael,
Unfortunately not in this course yet, that bundle is shiny new :) Yeah, if we're talking about the bundle - I think you're right, that's made by design, mostly because of the reasons I explained above. Though, I think you should be able to configure it, look at its config, IIRC you can set whatever token lifetime you want. Or yes, look for events where you can hook into.
Cheers!
Hi,
I would like to understand something better. The method onAuthenticationSuccess() allows redirection to the targetpath of session. But, if I am not wrong, this targetpath is null for all routes without access control roles. For example, if I am in the page of an article like an anonymous user and then I log in, the code redirect the user to homepage. Is it normal this flow? Normally it could come back to article page.
Thanks for your time.
Hey Gaetano S.
If the action you did before doesn't force you to login, then, after a successful login it will redirect you to the homepage by default but you can change it an choose any other route. Or, you can override that method and implement the behavior you want.
Cheers!
Hi,
I built a Controller with FB oAuth flow and at the end of this Controller, FB confirms the user email.
I search the user by this email and the user is correctly found and I would like to login automatically
I implemented LoginFormAuthenticator and when I follow the code of this page, I have nothing, no errors or warning and the user is not authenticated after it redirects to home page
Here the firewall :
Here is the controller :
I don't know what's wrong. Any idea ?
I found out by myself :-) that I had to create a new Guard with same entry point for FB and the classic login form.
Great tutorial. Thanks !
I could not get this to work, not matter what I tried. Turns out I had this set in security.yaml:
"anonymous: lazy"
I do like this setting, any way to make this manual authentication work while lazily loading the user object?
Hey Dirk
I just tried out the "lazy" option and it works. I get logged in upon registration. Did you upgrade your project to Symfony 4.4? I'm asking because this tutorial is based on Symfony 4.1 and the lazy option was introduced in 4.4 (https://symfony.com/blog/ne... )
Cheers!
Just a little mistake in the script:
First, a service called GuardAuthenticationHandler $guardHandler
It's actually GuardAuthenticatorHandler
The code in the video is completely correct but I guess I'm more of a verbal learner and I was stumped as to why I couldn't inject that class for 10 minutes :D
Hey Bunny_T
Thanks for catching it! Sometimes typos happen. It's already fixed, and soon everything will be correct!
Thanks again, Cheers!
I am not sure if I missed out, but I finished the interesting course and never found any where that allows users to change password, I know this can be as simple as an update to field of a table but I wonder if you have done it somewhere in Symfonycasts that I can have access to? Thank you as usual!
Hey Dung L.
I think we don't have that use case covered but it's not difficult to do it by yourself. You only need a form with basically two fields
First, field would be a repeated field type, where the user submit its new password (and confirms) https://symfony.com/doc/current/reference/forms/types/repeated.html
And second, a field where user can submit his current password https://symfony.com/doc/current/reference/constraints/UserPassword.html
Also, you should secure that route by checking the role IS_AUTHENTICATED_FULLY
it's super easy to do if you are using ACL
// config/packages/security.yaml
security:
access_control:
- { path: ^/profile/change-password$, roles: IS_AUTHENTICATED_FULLY }
And that's it! Cheers!
Hello!
I have just added the registration process to my site with automatic authentication at the end of the process.
In my controller, I have:
<br />$guard->authenticateUserAndHandleSuccess(<br /> $user,<br /> $request,<br /> $form_authenticator,<br /> 'main'<br />);<br />
The user is indeed authenticated at the end of the registration process but it remains on the registration page (the user clicked on a register link to get the form).
In my loginauthentication, in onAuthenticationSuccess method, I do have:
`/** @var User $user */
$user = $token->getUser();
if (in_array("ROLE_ADMIN", $user->getRoles()))
return new RedirectResponse($this->urlGenerator->generate('admin_dashboard'));
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) {
return new RedirectResponse($targetPath);
}
return new RedirectResponse($this->urlGenerator->generate('homepage'));`
If I understand well how it's working, it should do:
Is this correct? If yes, what did I miss? I would think that after registration, we are in the third case, no?
thx in advance!
Hey be_tnt
In theory you are right but that's not the way how you check for roles. You need to delegate that task to Symfony\Component\Security\Core\Security::isGranted()
. Also, could you add a dump()
on your onAuthenticationSuccess
method? So we can be sure that it's being executed
Cheers!
Thx Diego!
I made the change to isGranted.
I have also added 2 dump: one to see if targetpath has a value and one just before the last redirect. I got the 2 dump values: targetPath is null and the last dump value. So normally it should execute the last redirectResponse but in reality, the freshly registered user remains on register page.
Thx!
hummm I think I understood. Checking the guard->authenticateUserAndHandleSuccess method, I see that it does return the "redirect response" but it does not executed it. So I need one more step in the controller, execute the redirection :) Is this correct?
Oh, I think you just forgot to return the response of $guardHandler->authenticateUserAndHandleSuccess()
in your controller
Cheers!
Tip:
To deny access for Users to /login and /register (after a user is logged in), I redirect them in both methods to another route.
Otherwise a logged in user is able to go to /register and /login, even after he's logged in.
I played with IsGranted like this:
/**
* @IsGranted("IS_AUTHENTICATED_ANONYMOUSLY && !IS_AUTHENTICADED_FULLY, IS_AUTHENTICATED_ANONYMOUSLY && !IS_AUTHENTICADED_REMEMBERED")
*/
but it did not work.
If there's a way to let Users get "locked out" of /login and /register via @IsGranted, please let me know.
Hey Mike,
I think it's impossible to do via @IsGranted annotation because you need to do a redirect and that's why you need to write it in PHP. I'd suggest you to add an additional check for IS_AUTHENTICADED_FULLY in your loginAction() and if so - redirect to somewhere:
public function loginAction()
{
if ($this->isGranted('IS_AUTHENTICADED_FULLY')) {
return $this->redirectToRoute('some-route-name');
}
// ...
}
I think you do want to allow IS_AUTHENTICADED_REMEMBERED users to see your login page so they would be able to become IS_AUTHENTICADED_FULLY, otherwise they would not be able to get access to some part of your applications where you required IS_AUTHENTICADED_FULLY - they will be redirected to the login page, but it will redirect them somewhere else - sounds not OK.
I hope this helps!
Cheers!
To improve UX I would like to have the _remember_me field hidden and always set to "on".
Is there any downside of this?
By example, if you login to Amazon, close your browser and reopen it, you're always logged in. No "Remember Me" Button.
Just on some occasions you have to reenter your password to make sure you're really you.
Hey Mike P.
Read about the parameter always_remember_me
. I think it does exactly what you want: https://symfony.com/doc/current/security/remember_me.html
Cheers!
Looks like something wrong with make:registration-form command in a whole as we build registration form here by hand instead of Maker command.
I’ve made all the steps written here https://symfony.com/doc/cur...
and make:registration-form has produced exactly the same files and code. But when I submit registration form the $form in controller action is ALWAYS invalid. I.e. false === $form->isValid(). As you can see the reason is that data.password in null what is ConstraintViolation.
But it's really strange since 1) there is no 'password' field in RegistrationFormType and 2) all the constraints generated are for 'plainPassword' field. Seems like entity validation for User executes before form validation. But User entity itself does not have @Assert annotations out of the box and in my project as well.
Thus one needs 1) to remove generated $form->isValid() check or 2) use hand-made form and logic as described in tutorial above. If I remove $form->isValid() everything is fine. Exactly as in tutorial above.
Does it means that make:registration-form is not "production-ready" at the moment or I just missed something important using this command?
Hey Egor,
So, you don't have any password field in src/Form/RegistrationFormType.php but you see that error? Hm, do you have any constraints for $password field in User entity?
Cheers!
victor , it's amazing but I just created new Sf project from scratch and add all the code only with `./bin/console` and now it works fine. But in the project i mentioned above there was NO 'password' field in src/Form/RegistrationFormType.php . I haven't managed to fully xdebug the flow (since it stepped in too deep inside the framework code and i just lost control ) but examination of $form in controller showed that $form became invalid after $form->handleRequest(). But there should not be any 'password' in request!!! OK, looks like i add some code somewhere by mistake trying to write more "manual" code. And, yes, there were no constraints at all in User entity.
Hey Egor,
Hm, weird, difficult to say without debugging what was wrong. Glad it works in a new project
Cheers!
I guess this question is more about routing and forms, but I see this pattern a lot in tutorials and in the Symfony docs where we just have a form post to the same route that it's viewed from, and we have a single action that conditionally handles both GET
and POST
requests. That feels weird to me. Wouldn't it make more sense to have two separate actions, one tied to GET
requests and the other to POST requests? And in this particular case, wouldn't it be more appropriate for the registration form to POST
to something like /users
? I know it's sort of a side-issue for this authentication lesson, but it always trips me up a bit. Maybe the convention has to do with how the Symfony form component works with the handleRequest
and isSubmitted
methods.
Hey JackStr!
Great question! It's 100% up to you. Honestly, the only reason it's *typically* done in one action is because I subjectively made the decision many years ago to document the form component using this strategy on Symfony.com. And now... that's what you see everywhere ;). The downside of just one action is it's a bit harder to understand the flow. The benefit of one action is that two actions will require a little bit more duplication - a little bit more coding. But, it will work either way - there is no problem with separating the GET and POST into separate actions, and I get this question pretty regularly. Feel free to do what feels best for you :).
Cheers!
You only have to activate it in your security configuration. Give it a look to the docs: https://symfony.com/doc/cur...
Cheers!
i set `always_remember_me: true` in security.yaml and use `$guardHandler->authenticateUserAndHandleSuccess`, but after this rememberme cookie wasn't created
Hey unionelein !
Ah, that's an edge case I didn't think of when designing Guard! So, you're right - when you use this manual authentication method, it does not go through the remember me process. So, you will need to do it manually - let me see if I can help :). It's actually a bit trickier than it should be :/.
1) In your controller, add two new arguments RememberMeServicesInterface $rememberMeServices
and TokenStorageInterface $tokenStorage
2) For authentication, do this:
$response = $guardHandler->authenticationUserAndHandleSuccess(...);
$rememberMeServices->loginSuccess($request, $response, $tokenStorage->getToken());
return $response;
3) To make the $rememberMeServices argument work, go into config/services.yaml
. Add a key under services._defaults.bind
:
services:
_defaults:
# ...
bind:
$rememberMeServices: '@security.authentication.rememberme.services.simplehash.main'
Where the "main" part at the end should match your firewall name in config/packages/security.yaml
(it's called "main" by default).
Let me know if that works! We should really make that easier - it's just not something that occurred to me before!
Cheers!
We should really make that easier - it's just not something that occurred to me before!
Any news on that?
Because your posted answer doesn't work for me :(
...
Update:
It works, I just had to make sure the _remember_me field has to be set
Going to <b>register</b> route won't work when you are
logged out, if the route is not available for anonymous user. Am I right
?
I mean we need to add :- { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY }
in access_control
Hey José D.!
It depends :). The short answer, if you're following the code of this tutorial, is no - you do NOT need this. In our set up, every page is available anonymously... and THEN we start adding security to secure individual pages.
But, there is one important thing to check in your app. In OUR app, at this point, the access_control in security.yaml is empty. This means that, when each page loads, there are NO access_control entries to deny access. That means that every page is accessible anonymously (well, at least until you hit the security checks in the controllers themselves). However, in your app, if you DO have some access_control, then they may be causing access to be denied on your registration page. In that case, yes, you may need to add this entry to "whitelist" this one page. We actually talk about this in good detail in a super old tutorial (though the access_control behavior has NOT changed): https://symfonycasts.com/sc...
Let me know if that make sense... or if I just confused ;)
Cheers!
Hi! What if I want to redirect user after registration on different route from user who is logging in?
Hey Sasa,
Good question! So, you can do it in onAuthenticationSuccess() - there you have access to $request, so you can check if user is on registration page and if so - redirect him to a different route :)
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}
Why not just include EntityManagerInterface $em in the params? The only reason I can think is that we don't necessarily need it (if POST isn't submitted).