Symfony 4 login form with security and database users - authentication

I was a total noob on Symfony about a week ago and I thought I should just dive in Symfony 4. After a week of trying to solve the basic login problem, I believe the documentation is still missing some parts.
Now I've found a solution and I will share it along with some tips on what you might be doing wrong. First part of the answer is a list of suggestions, while the second part is the creation of a project with working login from scratch (supposing you already have composer installed and using a server like apache).

Part 1: Suggestions
403 Forbidden
Check the access_control: key in security.yaml. The order of the rules has impact, since no more than one rule will match each time. Keep most specific rules on top.
login_check
Make sure the form action sends you to the login_check path, or whatever you changed it to in security.yaml.
Also check that you have declared a route for the login_check path either in a controller or in routes.yaml.
input name
Symfony forms tend to encapsulate input names in an array, while it only expects them to be named _username and _password (you can change that in security.yaml) to count it as a login attempt. So inspect the inputs to make sure the name attributes are correct.
Part 2: Full Symfony 4 Login
Project Setup
Let's start by creating the project. Open cmd/terminal and go to the folder you want to contain the project folder.
cd .../MyProjects
composer create-project symfony/website-skeleton my-project
cd my-project
Now you have created a Symfony 4 website template in .../MyProjects/my-project and the cmd/terminal is in that path and will execute the rest of the commands properly.
Check in your .../MyProjects/my-project/public folder for a .htaccess file. If it exists you are fine, else run the following command.
composer require symfony/apache-pack
You can now find your site by visiting my-project.dev/public. If you want to remove this public path, you should do so using the .htaccess file, not moving the index.php.
Project Settings
1) Edit the DATABASE_URL key inside the .env file to correspond to your database settings.
2) Edit the config/packages/security.yaml file, so it looks like this:
security:
encoders:
App\Entity\User:
algorithm: bcrypt
providers:
user:
entity:
class: App\Entity\User
property: username
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
anonymous: true
provider: user
form_login:
#login_path: login
#check_path: login_check
default_target_path: homepage
#username_parameter: _username
#password_parameter: _password
logout:
#path: /logout
#target: /
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }
- { path: ^/admin, roles: ROLE_ADMIN }
Some explanation:
App\Entity\User is the User entity you 'll create in a while to handle the login.
The user provider is just a name that needs to have a match in providers and firewalls.
The logout key must be declared if you want to allow the user to... well, logout.
Values in #comment reveal the default value we'll be using later on and act as a reference of what you are more likely to change.
User Entity
A user must have a role, but could have more. So let's build a UserRole Entity first for a ManyToMany relationship.
php bin/console make:entity userRole
All entities start with an id property. Add a role too.
php bin/console make:entity user
User needs the username, password and roles properties, but you can add more.
Let's edit the src/Entity/User.php file:
Add the UserInterface interface to your User class.
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
Edit the generated getRoles(), to make it return string array.
public function getRoles(): array
{
$roles = $this->roles->toArray();
foreach($roles as $k => $v) {
$roles[$k] = $v->getRole();
}
return $roles;
}
getSalt() and eraseCredentials() are functions to implement the UserInterface interface.
public function getSalt()
{
return null;
}
public function eraseCredentials()
{
}
Using the bcrypt algorithm (as we set in security.yaml) we don't need a salt. It generates automatically one. No, you don't store this salt anywhere and yes, it will produce different hash for the same password every time. But yes, it will work somehow (magic...).
If you need a different algorithm, that uses salt, you need to add a salt property on the User entity.
Homepage
For testing purposes we will create a homepage
php bin/console make:controller homepage
Edit the generated src/Controller/HomepageController.php file to change the root to /
#Route("/", name="homepage")
Login Controller
php bin/console make:controller login
Edit the generated src/Controller/LoginController.php file to make it like this:
<?php
namespace App\Controller;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use App\Form\LoginType;
class LoginController extends Controller
{
/**
* #Route("/login", name="login")
*/
public function index(AuthenticationUtils $authenticationUtils)
{
$error = $authenticationUtils->getLastAuthenticationError();
$lastUsername = $authenticationUtils->getLastUsername();
$form = $this->createForm(LoginType::class);
return $this->render('login/index.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
'form' => $form->createView(),
]);
}
/**
* #Route("/logout", name="logout")
*/
public function logout() {}
/**
* #Route("/login_check", name="login_check")
*/
public function login_check() {}
}
Login Form
php bin/console make:form login
You don't have to associate it to the User entity.
Edit the generated src/Form/LoginType.php file to add this:
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
replace this:
$builder
->add('_username')
->add('_password', PasswordType::class)
->add('login', SubmitType::class, ['label' => 'Login'])
;
and add this function, to prevent Symfony from changing the input names you requested above by enclosing them in login[...]
public function getBlockPrefix() {}
Login Template
Edit the templates/login/index.html.twig file to add this code in the {% block body %} ... {% endblock %}:
{% if error %}
<div>{{ error.messageKey|trans(error.messageData, 'security') }}</div>
{% endif %}
{{ form_start(form, {'action': path('login_check'), 'method': 'POST'}) }}
{{ form_widget(form) }}
{{ form_end(form) }}
Database Generation
php bin/console doctrine:migrations:generate
php bin/console doctrine:migrations:migrate
This should have generated your database, according to your User and UserRole entities.
Generate Password
The following command will provide you with a hashed password you can directly insert into the database. The password will be hashed with the algorithm specified in security.yaml.
php bin/console security:encode-password my-password
Hope this helps!

Thank you very much, the documentation lacks some important things, but this works very good. For others, check the order of the access-control entries, because one misplaced entry may block the whole process.
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
This was working, but this not
access_control:
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }

Related

Symfony 6.2 does not reach token handler on token authentication

Symfony 6.2 does not reach custom token handler while executing a request if token_extractors equals to header in the security.yml.
Here is the security.yml
security:
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
providers:
access_token_provider:
entity:
class: App\Entity\AccessToken
property: secret
firewalls:
main:
lazy: true
provider: access_token_provider
stateless: true
pattern: ^/
access_token:
token_extractors: header
token_handler: App\Security\AccessTokenHandler
access_control:
- { path: ^/, roles: ROLE_ADMIN }
Here is the custom token handler, as you can see it shoud dump the token and die, and id does that if the token provided in query string.
namespace App\Security;
use App\Repository\AccessTokenRepository;
use Doctrine\ORM\NonUniqueResultException;
use SensitiveParameter;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
readonly class AccessTokenHandler implements AccessTokenHandlerInterface
{
public function __construct(private AccessTokenRepository $accessTokenRepository)
{
}
/**
* #throws NonUniqueResultException
*/
public function getUserBadgeFrom(#[SensitiveParameter] string $accessToken): UserBadge
{
var_dump($accessToken);
die;
$accessToken = $this->accessTokenRepository->getOneByToken($accessToken);
if (!$accessToken || !$accessToken->isValid()) {
throw new BadCredentialsException('Invalid credentials.');
}
return new UserBadge($accessToken->getId());
}
}
Authorization header generated by API client looks like this:
Authorization: Bearer 00000000-0000-0000-0000-000000000000
What's wrong in my implementation?
I tried to change token_extractors to query_string and provide token in GET parameters, and it does reach.
Query string looks like this:
localhost:8001/users?access_token=00000000-0000-0000-0000-000000000000
The problem was caused by Apache misconfiguration.
Apache didn't pass Authorization token to PHP due to default configuration and security reasons.
Adding CGIPassAuth On to a directory context solved this.
<Directory /var/www/html>
CGIPassAuth On
</Directory>
https://httpd.apache.org/docs/trunk/mod/core.html#cgipassauth

How to block url when the user has not verified their account ? (Symfony 6)

For my project (Symfony 6) I want to restrict some url as long as the user has not validated his account by clicking on the link received by email.
Because since i have make Registration (with symfony's website) I don't understand the interest of this point (account isVerified)
How to do this?
i've tried many things like modify security.yaml
You can give the user an additional role when they verify, then deny access if they don't have it in your controller like so:
// src/Controller/AdminController.php
// ...
public function adminDashboard(): Response
{
$this->denyAccessUnlessGranted('ROLE_VERIFIED');
// or add an optional message - seen by developers
$this->denyAccessUnlessGranted('ROLE_VERIFIED', null, 'User tried to access a page without having ROLE_VERIFIED');
}
https://symfony.com/doc/current/security.html#securing-controllers-and-other-code
You can also deny an entire pattern in security.yml:
# config/packages/security.yaml
security:
# ...
access_control:
# matches /users/verfied/*
- { path: '^/users/verfied', roles: ROLE_VERIFIED}
# matches /users/* except for anything matching the above rule
- { path: '^/users', roles: ROLE_USER }
https://symfony.com/doc/current/security.html#securing-url-patterns-access-control

Symfony2 hold behavior when extending DefaultAuthenticationSuccessHandler

I want to be able to login in my app via the usual login form and using AJAX directly sending the user/password to the login_check path.
The idea is when user access via AJAX return Response with code 200 or 400 depending if the login success or fails.
Looking at other threads I finally decide to extend the default handlers DefaultAuthenticationSuccessHandler and DefaultAuthenticationFailureHandler to achieve this, export as services and specify in the success_handler property in my secure area.
File service.yml
services:
authentication.success_handler:
class: %mycustom.authentication.success_handler.class%
arguments: ['#security.http_utils', {} ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
authentication.failure_handler:
class: %mycustom.authentication.failure_handler.class%
arguments: ['#http_kernel', '#security.http_utils', {}, '#logger' ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
File security.yml
secured_meems_area:
pattern: ^/somrurl/
form_login:
login_path: /somrurl/login
check_path: /somrurl/api/login_check
success_handler: authentication.success_handler
failure_handler: authentication.failure_handler
require_previous_session: false
All this seems to work, except the behaviour of my extendend handler isn't like the original one. In the default implementation used by Symfony if you access a page/A and you are not logged on, Symfony redirects to the login page and after it you are redirected again to page/A. But this not occurs with my extended handler.
To solve it, I can specify a default_target_path when registering the handler as a service, but I want to understand why it doesn't follow the "normal" behavior.
Any ideas out there.
Thanks in advance.
SOLUTION !!!
After looking and test I found a solution here Symfony2 extending DefaultAuthenticationSuccessHandler
The idea is override the default symfony success/failure handlers instead of define my own and apply in the security.yml file.
File security.yml
secured_meems_area:
pattern: ^/somrurl/
form_login:
login_path: /somrurl/login
check_path: /somrurl/api/login_check
#
# DON'T USE !!!
#
# success_handler: authentication.success_handler
# failure_handler: authentication.failure_handler
#
require_previous_session: false
File service.yml. (NOTE the security. in the service name)
services:
security.authentication.success_handler:
class: %mycustom.authentication.success_handler.class%
arguments: ['#security.http_utils', {} ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
security.authentication.failure_handler:
class: %mycustom.authentication.failure_handler.class%
arguments: ['#http_kernel', '#security.http_utils', {}, '#logger' ]
public: false
tags:
- { name: 'monolog.logger', channel: 'security' }
This way we are overriden the default implementation with our own and don't need to specify the handlers in the security area.

Symfony2 - Redirection after successful login

I've just started working through the Symfony 2 tutorials. I have created a bundle with a user class and have tried to follow the instructions to set up a login process. I think I am nearly there, however I'm currently falling at the last hurdle.
I have set up a bundle: Dan\AuthBundle, which contains my user class and another bundle: Dan\HelloBundle which I want to allow only logged in users to access.
My security.yml file is as follows:
security:
encoders:
Dan\AuthBundle\Entity\User: sha512
providers:
main:
entity: { class: Dan\AuthBundle\Entity\User, property: username }
administrators:
entity: { class: DanAuthBundle:User }
firewalls:
secured_area:
pattern: ^/*
form_login:
check_path: /login_check
login_path: /login
always_use_default_target_path: false
default_target_path: /hello
access_control:
- { path: ^/hello/.* }
The main routing.yml file looks like this:
DanAuthBundle:
resource: "#DanAuthBundle/Resources/config/routing.yml"
prefix: /auth/
DanHelloBundle_homepage:
pattern: /hello/
defaults: { _controller: DanHelloBundle:Default:index }
login:
pattern: /login
defaults: {_controller: DanAuthBundle:Default:login }
login_check:
pattern: /login_check
I have created several instances of my user class manually.
If I try to access the url /hello, I correctly get redirected to the login page. If I enter incorrect details, I get the correct message(s) delivered in the template, however, when I log in with the correct details, I receive a 324 (empty response) error (at this time, the url displayed in the browser is login_check).
From reading the documentation, I thought I should be redirected to the page I was originally trying to access?
http://symfony.com/doc/current/book/security.html#using-a-traditional-login-form
By default, if the submitted credentials are correct, the user will be
redirected to the original page that was requested (e.g. /admin/foo).
If the user originally went straight to the login page, he'll be
redirected to the homepage. This can be highly customized, allowing
you to, for example, redirect the user to a specific URL.
Also, if I try to access the page after entering the correct details, I once again get redirected to the login page.
Can anyone see if I've missed anything obvious?
This is from my log file:
[2012-06-18 18:33:47] doctrine.DEBUG: SELECT t0.id AS id1, t0.username
AS username2, t0.salt AS salt3, t0.hashed_password AS hashed_password4
FROM User t0 WHERE t0.username = ? (["hello"]) [] [] [2012-06-18
18:33:47] security.INFO: User "hello" has been authenticated
successfully [] [] [2012-06-18 18:33:47] event.DEBUG: Listener
"Symfony\Component\Security\Http\Firewall::onKernelRequest" stopped
propagation of the event "kernel.request". [] [] [2012-06-18 18:33:47]
event.DEBUG: Listener
"Symfony\Bundle\FrameworkBundle\EventListener\RouterListener" was not
called for event "kernel.request". [] [] [2012-06-18 18:33:47]
event.DEBUG: Listener
"Symfony\Bundle\AsseticBundle\EventListener\RequestListener" was not
called for event "kernel.request". [] [] [2012-06-18 18:33:47]
event.DEBUG: Notified event "kernel.response" to listener
"Symfony\Component\Security\Http\Firewall\ContextListener::onKernelResponse".
[] [] [2012-06-18 18:33:47] security.DEBUG: Write SecurityContext in
the session [] []
Any advice appreciated.
Thanks.
// if you're using Symfony 2.0
$key = '_security.target_path';
// if you're using Symfony 2.1 or greater
// where "main" is the name of your firewall in security.yml
$key = '_security.main.target_path';
// try to redirect to the last page, or fallback to the homepage
if ($this->container->get('session')->has($key)) {
$url = $this->container->get('session')->get($key);
$this->container->get('session')->remove($key);
} else {
$url = $this->container->get('router')->generate('homepage');
}
return new RedirectResponse($url);
You need 2 listeners.
One to set in session last page
Second to redirect after succesfull login
That link will solve your problem: http://www.reecefowell.com/2011/10/26/redirecting-on-loginlogout-in-symfony2-using-loginhandlers/
just use getUser check in your respective action (where you are rendering the login form view) as below:
if($this->getUser()){
return $this->redirect($this->generateUrl('your-redirect-path-alise'));
}

Symfony2 - Access control

I want to set access_control for everything, but not for the pattern /login (and maybe some more routes).
So I set up the path for login first, with the role IS_AUTHENTICATED_ANONYMOUSLY.
Then I set up the path for everything, with the role IS_AUTHENTICATED_FULLY.
The problem is that login form is now generated twice. I see two login fields, two submit buttons, and so on.
If I remove the access_control rules I have no, and only set to one path like /blog/.* and with role: IS_AUTHENTICATED_FULLY, it works fine, the login form is like it should be.
So my configuration of "you need to be authenticated everywhere except here" does not work.
This is what I tried to do when login form is generated twice:
access_control:
- { path: /login, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /.*, role: IS_AUTHENTICATED_FULLY }
Does anyone have any good examples for how to set up such a rule?
So I got the answer :) So if anyone should have this problem, you also have to let _wdt, and _profiler being accessed anonymously :)
access_control:
- { path: /_wdt/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /_profiler/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /login, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /.*, role: IS_AUTHENTICATED_FULLY }
Don't forget to add this firewall:
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false