Implement your own SecurityController
I recently wrote an article for my company on managing users and their authentication natively using the Symfony2 Core, and therefore without FOSUserBundle. Following this already rich first article, I wanted to describe a second equally useful part which will allow us to quickly set up the essential functions, namely, resetting your password, changing your password, validating your account or even register. Actions just as essential when you have a system managing users.
Set up a SecurityController
First, if you haven't followed the first tutorial on how to set up user management, I advise you to take a look. If you followed the solution, you should logically have a SecurityController or something else. In my case, I only have three methods, only one of which is really usable.
- loginAction
This method connects a user. - checkAction
This method simply allows you to declare a route for the firewall, allowing a user to connect on the server side. - logoutAction
This method is used to declare a route for the firewall allowing a user to be disconnected.
Open our platform to new users
It would be particularly interesting if our users could connect, and therefore be able to register beforehand.
First, we generate the form with the information you want to ask your user.
Regarding the form you will use to register your user, know that the field " Password should not be in your form. However, you will need to add two fields " not mapped in order to have the desired password entered twice.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
/ **
* @Method({“GET”})
* @Road(“/register”, name=”register”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function register()
{
$form = $ this->createForm(new UserType(UserType::REGISTER), new User());
return array(
“form” => $form->createView(),
);
}
|
Then, it's when your user comes back with his form filled out that we'll have to register him or reject his file :p
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
/ **
* @Method({“POST”})
* @Road(“/register”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function registerNow(Request $request)
{
$params = $request->request->all()[“name_of_my_form”];
$form = $ this->createForm(new UserType(UserType::REGISTER), new User());
$form->submit($request);
if (array_key_exists(“plain_password”, $params) && array_key_exists(“plain_password2”, $params) && $params[“plain_password”] == $params[“plain_password2”]) {
if ($form->isValid()) {
$data = $form->getData();
$data->setPassword($ this->container->get(“security.encoder_factory”)->getEncoder($data)->encodePassword($params[“plain_password”], $data->getSalt()));
$em->persist($data);
$em->flush();
return $ this->redirect($ this->generateUrl(“login”, array(" message " => “You have received an email to validate your account. »)));
}
}
return array(
“errors” => $params[“plain_password”] == $params[“plain_password2”]? $form->getErrors(): array(“The two passwords must be the same”),
“form” => $form->createView(),
);
}
|
Here we will quickly detail the workflow to register a new user.
- We look if all the required fields are entered correctly, including the two "password" fields, and if the latter two are identical.
- We encode the password and we “set” it in the entity.
- In case of any error, we return the form with the error that we think should be detailed.
Create a feature to reset your password
Now your user can log in but what will we do if he loses his password. It is obvious that we are not going to set up a contact email address dedicated to its useless operations.
As you have seen above, I usually declare two methods for each of my functionalities: one of my methods is responsible for managing the view created following a request GET and a result of a request POST. You can absolutely concatenate these two methods into one and the same method.
1
2
3
4
5
6
7
8
9
|
/ **
* @Method({“GET”})
* @Road(“/reset”, name=”reset”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function reset() {
return array();
}
|
In a second step, we will declare the complementary method managing the requests POST.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
/ **
* @Method({“POST”})
* @Road(“/reset”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
*/
public function resetNow(Request $request)
{
$params = $request->request->all();
if (!array_key_exists(“login”, $params)) {
throw new Exception(“No login given”);
}
$login = &$params[“login”];
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user = $em->getRepository(“NamespaceMyBundle:User”)->findOneBy(array(“login” => $login));
if ($user == null) {
return $ this->redirect($ this->generateUrl(“login”, array()));
}
$password = “myRandowPassword”;
$user->setPassword($ this->container->get(“security.encoder_factory”)->getEncoder($user)->encodePassword($password, $user->getSalt()));
$em->persist($user);
$em->flush();
// We send the password by email
return $ this->redirect($ this->generateUrl(“login”, array()));
}
|
This method was designed to reset the password of a user who provided his login/username. In my case, the password was then sent by email. I'll let you add that gallant line.
- So we're going search user.
- We generate a password that we come to inform in the user once he has encoded according to the rules that you have defined.
Set up password change
At this point, our user can generate a new password if it was lost, but in case he just wants to change it, we need a gate to define a gate.
1
2
3
4
5
6
7
8
9
|
/ **
* @Method({“GET”})
* @Road(“/change”, name=”change-password”)
* @Secure(roles=”IS_AUTHENTICATED_FULLY”)
* @Template()
*/
public function change() {
return array();
}
|
Here is the code to generate the view. First, you will have to enter your old password, then enter your new password twice. The second time being confirmation.
Now we will see the code to resetter the password. THE to process is similar to generating a new random password.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
/ **
* @Method({“POST”})
* @Road(“/change”)
* @Secure(roles=”IS_AUTHENTICATED_FULLY”)
* @Template()
*/
public function changeNow(Request $request)
{
$params = $request->request->all();
if (!array_key_exists(“current”, $params)
|| !array_key_exists(“new”, $params)
|| !array_key_exists(“new2”, $params))
{
return array("Error" => “Please fill all fields”);
}
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user= $ this->getUser();
$user_encoders = $ this->container->get(“security.encoder_factory”)->getEncoder($user);
$user_repository = $em->getRepository(“NamespaceMyBundle:User”);
$current_password_encoded = $user_encoders->encodePassword($params[“current”], $user->getSalt());
$new_password_encoded = $user_encoders->encodePassword($params[“new”], $user->getSalt());
if ($user_repository->findOneBy(array(“password” => $current_password_encoded)) == null) {
return array("Error" => “The current password is wrong”);
} elseif ($params[“new”] != $params[“new2”]) {
return array("Error" => "The two fields password aren't the same");
}
$user->setPassword($new_password_encoded);
$em->persist($user);
$em->flush();
return $ this->redirect($ this->generateUrl(“logout”, array()));
}
|
If you take 1 minute to read the code, you will see that this one is particularly simple.
- First, we check if the three fields (old password, new password and confirmation) have been entered correctly.
- On enter the password current and we compare it with the current passwordment in the database to see if it corresponds with the old password entered.
- We check if the "two" new passwords are identical.
- Enter the new password and push in the entity.
Activation of his account
This feature is not detailed perfectly in other snippets above. Its purpose is to unblock a user who has just registered, when he has validated his email for example. This functionality is developed on almost all the platforms we know for several reasons. To set up a blocking of a user, you might also need to implement a provider.
- Block/limit accounts fake and spam.
- Vérifier that the user has filled in an e-mail address that appears usable at first sight.
- Remove accounts that have not been validated after a certain period of time.
Workflow
- A user registers. His account is then blocked via a field specific to you. This field should then prevent him from connecting as long as this field indicates that the account is disabled.
1
2
3
4
5
6
7
8
|
// NamespaceMyBundleEntityUser
class User {
public function __construct() {
$ this->token = hash(“sha512”, uniqid());
}
...
}
|
1
|
$user->setEnabled(false);
|
- The user received an email when his profile was flush in database. This email must be part of an address that you generate.
In this road, a token or unique identifier must be given allowing the user concerned to be found. I advise you to use a UUID4 which is intended to be random. You can find the list of UUIDs as well as the description of all versions.
1
2
3
4
|
/ **
* @Road(“/activate”, name=”activate”)
*/
public function activated() {…}
|
1
|
$ this->generateUrl(“activate”, array(« token » => $user->getToken()), true);
|
You should have a URL like this.
1
|
http://hostname/activate?token=myUniqueToken
|
- The user opens his email and tries to activate his account by clicking on the link provided. We then enter the process below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
/ **
* @Method({“GET”})
* @Road(“/activate”, name=”activate”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function activated(Request $request) {
$params = array();
$token = $request->query->get(« token »);
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user = $em->getRepository(“NamespaceMyBundle:User”)->findOneBy(array(« token » => $token));
if ($user != null) {
$user->setEnabled(true);
$em->persist($user);
$em->flush();
$params[“activate”] = true;
} else {
$params[“activate”] = false;
}
return $params;
}
|
With this process you should have no problem enabling user account validation in place.
You can find this equivalent code on this Gist.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
use JMSSecurityExtraBundleAnnotationSecure;
use SensioBundleFrameworkExtraBundleConfigurationMethod;
use SensioBundleFrameworkExtraBundleConfigurationRoad;
use SensioBundleFrameworkExtraBundleConfigurationtemplate;
use symfonyBundleFrameworkBundleControllerController;
use symfonyComponentHttpFoundationRequest;
use symfonyComponentSecurityCoreSecurityContext;
class SecurityController extends Controller
{
/ **
* @Method({“GET”})
* @Road(“/login”, name=”login”)
* @Template()
*/
public function login(Request $request)
{
$request= $ this->getRequest();
$session = $request->getSession();
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
$params = array(
“last_username” => $session->get(SecurityContext::LAST_USERNAME),
"Error" => $error,
" message " => $request->get(" message "),
);
if ($request->isXmlHttpRequest()) {
return $ this->render(“GCDirectoryMainBundle:Security:login-ajax.html.twig”, $params);
}
return $params;
}
/ **
* @Method({“POST”})
* @Road(“/login_check”, name=”login_check”)
*/
public function check()
{
throw new RuntimeException('You must configure the check path to be handled by the firewall using form_login in your security firewall configuration.');
}
/ **
* @Method({“GET”})
* @Road(“/logout”, name=”logout”)
*/
public function logout()
{
throw new RuntimeException('You must activate the logout in your security firewall configuration.');
}
/ **
* @Method({“GET”})
* @Road(“/reset”, name=”reset”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function reset() {
return array();
}
/ **
* @Method({“POST”})
* @Road(“/reset”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
*/
public function resetNow(Request $request)
{
$params = $request->request->all();
if (!array_key_exists(“login”, $params)) {
throw new Exception(“No login given”);
}
$login = &$params[“login”];
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user = $em->getRepository(“NamespaceMyBundle:User”)->findOneBy(array(“login” => $login));
if ($user == null) {
return $ this->redirect($ this->generateUrl(“login”, array()));
}
$password = “myRandowPassword”;
$user->setPassword($ this->container->get(“security.encoder_factory”)->getEncoder($user)->encodePassword($password, $user->getSalt()));
$em->persist($user);
$em->flush();
return $ this->redirect($ this->generateUrl(“login”, array()));
}
/ **
* @Method({“GET”})
* @Road(“/change”, name=”change-password”)
* @Secure(roles=”IS_AUTHENTICATED_FULLY”)
* @Template()
*/
public function change() {
return array();
}
/ **
* @Method({“POST”})
* @Road(“/change”)
* @Secure(roles=”IS_AUTHENTICATED_FULLY”)
* @Template()
*/
public function changeNow(Request $request)
{
$params = $request->request->all();
if (!array_key_exists(“current”, $params)
|| !array_key_exists(“new”, $params)
|| !array_key_exists(“new2”, $params))
{
return array("Error" => “Please fill all fields”);
}
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user= $ this->getUser();
$user_encoders = $ this->container->get(“security.encoder_factory”)->getEncoder($user);
$user_repository = $em->getRepository(“NamespaceMyBundle:User”);
$current_password_encoded = $user_encoders->encodePassword($params[“current”], $user->getSalt());
$new_password_encoded = $user_encoders->encodePassword($params[“new”], $user->getSalt());
if ($user_repository->findOneBy(array(“password” => $current_password_encoded)) == null) {
return array("Error" => “The current password is wrong”);
} elseif ($params[“new”] != $params[“new2”]) {
return array("Error" => "The two fields password aren't the same");
}
$user->setPassword($new_password_encoded);
$em->persist($user);
$em->flush();
return $ this->redirect($ this->generateUrl(“logout”, array()));
}
/ **
* @Method({“GET”})
* @Road(“/register”, name=”register”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function register()
{
$form = $ this->createForm(new UserType(UserType::REGISTER), new User());
return array(
“form” => $form->createView(),
);
}
/ **
* @Method({“POST”})
* @Road(“/register”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function registerNow(Request $request)
{
$params = $request->request->all()[“name_of_my_form”];
$form = $ this->createForm(new UserType(UserType::REGISTER), new User());
$form->submit($request);
if (array_key_exists(“plain_password”, $params) && array_key_exists(“plain_password2”, $params) && $params[“plain_password”] == $params[“plain_password2”]) {
if ($form->isValid()) {
$data = $form->getData();
$data->setPassword($ this->container->get(“security.encoder_factory”)->getEncoder($data)->encodePassword($params[“plain_password”], $data->getSalt()));
$em->persist($data);
$em->flush();
return $ this->redirect($ this->generateUrl(“login”, array(" message " => “You have received an email to validate your account. »)));
}
}
return array(
“errors” => $params[“plain_password”] == $params[“plain_password2”]? $form->getErrors(): array(“The two passwords must be the same”),
“form” => $form->createView(),
);
}
/ **
* @Method({“GET”})
* @Road(“/activate”, name=”activate”)
* @Secure(roles=”IS_AUTHENTICATED_ANONYMOUSLY”)
* @Template()
*/
public function activated(Request $request) {
$params = array();
$token = $request->query->get(« token »);
$em = $ this->container->get(“doctrine.orm.default_entity_manager”);
$user = $em->getRepository(“NamespaceMyBundle:User”)->findOneBy(array(« token » => $token));
if ($user != null) {
$user->setActive(User::ACTIVE_ACTIVE);
$em->persist($user);
$em->flush();
$params[“activate”] = true;
} else {
$params[“activate”] = false;
}
return $params;
}
}
|