gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
We built this log in form by making a route, controller and rendering a template:
... lines 1 - 8 | |
class SecurityController extends AbstractController | |
{ | |
/** | |
* @Route("/login", name="app_login") | |
*/ | |
public function login(): Response | |
{ | |
return $this->render('security/login.html.twig'); | |
} | |
} |
Dead simple. When we submit the form, it POSTs right back to /login
. So, to authenticate the user, you might expect us to put some logic right here: like if this is a POST request, read the POSTed email & password, query for the User
object... and eventually check the password. That makes perfect sense! And that is completely not what we're going to do.
Symfony's authentication system works in a... bit of a magic way, which I guess is fitting for our site. At the start of every request, before Symfony calls the controller, the security system executes a set of "authenticators". The job of each authenticator is to look at the request, see if there is any authentication information that it understands - like a submitted email and password, or an API key that's stored on a header - and if there is, use that to query the user and check the password. If all that happens successfully then... boom! Authentication complete.
Our job is to write and activate these authenticators. Open up config/packages/security.yaml
. Remember the two parts of security: authentication (who you are) and authorization (what you can do).
The most important part of this file is firewalls
:
security: | |
... lines 2 - 13 | |
firewalls: | |
dev: | |
pattern: ^/(_(profiler|wdt)|css|images|js)/ | |
security: false | |
main: | |
lazy: true | |
provider: app_user_provider | |
# activate different ways to authenticate | |
# https://symfony.com/doc/current/security.html#firewalls-authentication | |
# https://symfony.com/doc/current/security/impersonating_user.html | |
# switch_user: true | |
... lines 27 - 33 |
A firewall is all about authentication: its job is to figure out who you are. And, it usually makes sense to have only one firewall in your app... even if there are multiple different ways to authenticate, like a login form and an API key and OAuth.
But... woh woh woh. If we almost always want only one firewall... why are there are already two? Here's how this works: at the start of each request, Symfony goes down the list of firewalls, reads the pattern
key - which is a regular expression - and finds the first firewall whose pattern matches the current URL. So there's only ever one firewall active per request.
If you look closely, this first firewall is a fake! It basically matches if the URL starts with /_profiler
or /_wdt
... and then sets security to false
:
security: | |
... lines 2 - 13 | |
firewalls: | |
dev: | |
pattern: ^/(_(profiler|wdt)|css|images|js)/ | |
security: false | |
... lines 18 - 33 |
In other words, it's basically making sure that you don't create a security system that is so epically awesome that... you block the web debug toolbar and profiler.
So... in reality, we only have one real firewall called main
. It has no pattern
key, which means that it will match all requests that don't match the dev
firewall. Oh, and the names of these firewalls - main
and dev
? They're totally meaningless.
Most of the config that we're going to put beneath the firewall relates to activating authenticators: those things that execute early in each request and try to authenticate the user. We'll add some of that config soon. But these two top keys do something different. lazy
allows the authentication system to not authenticate the user until it needs to and provider
ties this firewall to the user provider we talked about earlier. You should have both of these lines... but neither are terribly important:
security: | |
... lines 2 - 13 | |
firewalls: | |
... lines 15 - 17 | |
main: | |
lazy: true | |
provider: app_user_provider | |
... lines 21 - 33 |
Anyways, anytime that we want to authenticate the user - like when we submit a login form - we need an authenticator. There are some core authenticator classes that we can use, including one for login forms.... and I'll show you some of those later. But to start, let's build our own authenticator class from scratch.
To do that, go to terminal and run:
symfony console make:auth
As you can see, you can select "Login form authenticator" to cheat and generate a bunch of code for a login form. But since we're building things from scratch, select "Empty authenticator" and call it LoginFormAuthenticator
.
Awesome. This did two things: it created a new authenticator class and also updated security.yaml
. Open the class first: src/Security/LoginFormAuthenticator.php
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
public function supports(Request $request): ?bool | |
{ | |
// TODO: Implement supports() method. | |
} | |
public function authenticate(Request $request): PassportInterface | |
{ | |
// TODO: Implement authenticate() method. | |
} | |
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |
{ | |
// TODO: Implement onAuthenticationSuccess() method. | |
} | |
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |
{ | |
// TODO: Implement onAuthenticationFailure() method. | |
} | |
... lines 33 - 43 | |
} |
The only rule about an authenticator is that it needs to implement AuthenticatorInterface
... though usually you'll extend AbstractAuthenticator
... which implements AuthenticatorInterface
for you:
... lines 1 - 8 | |
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |
... lines 10 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
... lines 14 - 43 | |
} |
We'll talk about what these methods do one-by-one. Anyways, AbstractAuthenticator
is nice because it implements a super boring method for you.
Once we activate this new class in the security system, at the beginning of every request, Symfony will call this supports()
method and basically ask:
Do you see authentication information on this request that you understand?
To prove that Symfony will call this, let's just dd('supports')
:
... lines 1 - 11 | |
class LoginFormAuthenticator extends AbstractAuthenticator | |
{ | |
public function supports(Request $request): ?bool | |
{ | |
dd('supports!'); | |
} | |
... lines 18 - 43 | |
} |
Okay, so how do we activate this authenticator? How do we tell our firewall that it should use our new class? Back in security.yaml
, we already have the code that does that! This custom_authenticator
line was added by the make:auth
command:
security: | |
... lines 2 - 13 | |
firewalls: | |
... lines 15 - 17 | |
main: | |
... lines 19 - 20 | |
custom_authenticator: App\Security\LoginFormAuthenticator | |
... lines 22 - 34 |
So if you have a custom authenticator class, this is how you activate it. Later, we'll see that you can have multiple custom authenticators if you want.
Anyways, this means that our authenticator is already active! So let's try it. Refresh the login page. It hits the supports()
method! In fact, if you go to any URL it will hit our dd()
. On every request, before the controller, Symfony now asks our authenticator if it supports authentication on this request.
Next let's fill in the authenticator logic and get our user logged in!
Hey Maxim!
Ah, you're right! It only affects empty authenticator, with login form authenticator it's already there. I just created a PR in maker-bundle to fix it: https://github.com/symfony/... - feel free to give a review ;)
Cheers!
Hi,
after bin/console make:auth
I get following error:
The service "security.command.debug_firewall" has a dependency on a non-existent service "App\Security\LoginFormAuthenticator".
I'm using Symfony 5.4.11
console output:
php bin/console make:auth
What style of authentication do you want? [Empty authenticator]:
[0] Empty authenticator
[1] Login form authenticator
> 0
0
The class name of the authenticator to create (e.g. AppCustomAuthenticator):
> LoginFormAuthenticator
Which firewall do you want to update? [pimcore_admin_webdav]:
[0] pimcore_admin_webdav
[1] pimcore_admin
[2] wir_beten_fw
> 2
2
created: src/Security/LoginFormAuthenticator.php
updated: config/packages/security.yaml
Hey Stefan,
I believe you don't have enabled the autoconfigure feature. Do you have these lines in your config/services.yaml
file?
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
My config/services.yaml:
services:
# default configuration for services in *this* file
_defaults:
# automatically injects dependencies in your services
autowire: true
# automatically registers your services as commands, event subscribers, etc.
autoconfigure: true
# this means you cannot fetch services directly from the container via $container->get()
# if you need to do this, you can override this setting on individual services
public: false
Yes I do. So it should work - but it does not?
What should I add to configure LoginFormAuthenticator ?
I also updated to Symfony 5.4.14 - same problem.
Ok, let's review a few things.
First: double-check that the file's name matches to the class name of your LoginFormAuthenticator
Second: Double-check its namespace, it should be App\Security
If everything is ok, could you show me your config/packages/security.yaml
file? There might be a hint
Hi MolloKhan,
thanks for your time and help...
filenames - check
namespace - check
doublecheck - check
-security.yaml - sure - appended afterwards.
It's a pimcore application where I try to extend security for some pages
I want to configure firewall: 'wir_beten_fw'
path '/intern/' is working and redirecting to 'beter_login'
security:
enable_authenticator_manager: true
password_hashers:
App\Model\DataObject\User: website_beten.security.password_hasher_factory
providers:
pimcore_admin:
id: Pimcore\Bundle\AdminBundle\Security\User\UserProvider
wir_beten_provider:
id: website_beten.security.user_provider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
# Pimcore WebDAV HTTP basic // DO NOT CHANGE!
pimcore_admin_webdav:
pattern: ^/admin/asset/webdav
provider: pimcore_admin
http_basic: ~
# Pimcore admin form login // DO NOT CHANGE!
pimcore_admin:
pattern: ^/admin(/.*)?$
# admin firewall is stateless as we open the admin
# session on demand for non-blocking parallel requests
stateless: true
provider: pimcore_admin
login_throttling:
max_attempts: 3
interval: '5 minutes'
logout:
path: pimcore_admin_logout
target: pimcore_admin_login
custom_authenticators:
- Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminLoginAuthenticator
- Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminTokenAuthenticator
- Pimcore\Bundle\AdminBundle\Security\Authenticator\AdminSessionAuthenticator
two_factor:
auth_form_path: /admin/login/2fa # Path or route name of the two-factor form
check_path: /admin/login/2fa-verify # Path or route name of the two-factor code check
default_target_path: /admin # Where to redirect by default after successful authentication
always_use_default_target_path: false # If it should always redirect to default_target_path
auth_code_parameter_name: _auth_code # Name of the parameter for the two-factor authentication code
trusted_parameter_name: _trusted # Name of the parameter for the trusted device option
multi_factor: false # If ALL active two-factor methods need to be fulfilled (multi-factor authentication)
# demo_cms firewall is valid for the whole site
wir_beten_fw:
pattern: (/.*)?/intern(/.*)?$
# the provider defined above
provider: wir_beten_provider
form_login:
enable_csrf: true
login_path: beter_login
check_path: beter_login
failure_path: beter_login
default_target_path: login_ueberblick
always_use_default_target_path: true
password_parameter: _password
username_parameter: _username
custom_authenticator: App\Security\LoginFormAuthenticator
# login_throttling:
# max_attempts: 3
# interval: '5 minutes'
access_control:
# Pimcore admin ACl // DO NOT CHANGE!
- { path: ^/admin/settings/display-custom-logo, roles: PUBLIC_ACCESS }
- { path: ^/admin/login/2fa-verify, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS}
- { path: ^/admin/login/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS}
- { path: ^/admin/login$, roles: PUBLIC_ACCESS }
- { path: ^/admin/login/(login|lostpassword|deeplink|csrf-token)$, roles: PUBLIC_ACCESS }
- { path: ^/admin, roles: ROLE_PIMCORE_USER }
# interner Bereich - nur mit login weiter
- { path: /intern/, roles: [ROLE_PIMCORE_USER, ROLE_USER] }
role_hierarchy:
# Pimcore admin // DO NOT CHANGE!
ROLE_PIMCORE_ADMIN: [ROLE_PIMCORE_USER]
some stuff from config.yaml
models:
class_overrides:
'Pimcore\Model\DataObject\User': 'App\Model\DataObject\User'
security:
factory_type: password_hasher
password_hasher_factories:
App\Model\DataObject\User: website_beten.security.password_hasher_factory
Your config looks good to me. For some reason your authenticator is not being auto-registered. Double-check in your config/services.yaml
if you're not excluding it under the key:
services:
App\:
exclude: ...
Also try clearing the cache manually rm -rf var/cache
Don't know why - but this was missing in config/services.yaml:
App\Security\:
resource: '../src/Security/*'
so now - LoginFormAuthenticator - is found and executed
I can follow the tut
Thanks a lot
Hi
I have a question, how to secure form against changes value in hidden fields?
Hey Tomas,
Perhaps what you need here is a custom validation constraint. Here you can learn how to create own yourself https://symfony.com/doc/current/validation/custom_constraint.html
Cheers!
Hey
not, I'm looking for something what protect my form against "html" code manipulation
In cakephp is special component, maybe in symfony is something similarar
FormProtect
regards
It seems to me that that component does something similar to what you can achieve with custom validation constraints. The only difference is that you have to attach the validations to your entity fields.
Here you can learn how to create your own validators https://symfonycasts.com/screencast/symfony-forms/custom-validator
the tutorial is based on Symfony 4, but nothing meaningful has changed since then
Hi, if symfony calls the authenticator on each request, which is the reason for lazy parameter?
Hey GianlucaF,
Hm, yes, authenticator is called on each request... but we only call that supports() method on it, which returns true *only* on login route when we're sending a POST request to it, right? But if supports() returns false - we won't continue.
Cheers!
Hey Maciej,
Thank you for your feedback! We're really happy to hear it was useful for you :)
Cheers!
// composer.json
{
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-iconv": "*",
"babdev/pagerfanta-bundle": "^3.3", // v3.3.0
"composer/package-versions-deprecated": "^1.11", // 1.11.99.4
"doctrine/annotations": "^1.0", // 1.13.2
"doctrine/doctrine-bundle": "^2.1", // 2.6.3
"doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
"doctrine/orm": "^2.7", // 2.10.1
"knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
"knplabs/knp-time-bundle": "^1.11", // v1.16.1
"pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
"pagerfanta/twig": "^3.3", // v3.3.0
"phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
"scheb/2fa-bundle": "^5.12", // v5.12.1
"scheb/2fa-qr-code": "^5.12", // v5.12.1
"scheb/2fa-totp": "^5.12", // v5.12.1
"sensio/framework-extra-bundle": "^6.0", // v6.2.0
"stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
"symfony/asset": "5.3.*", // v5.3.4
"symfony/console": "5.3.*", // v5.3.7
"symfony/dotenv": "5.3.*", // v5.3.8
"symfony/flex": "^1.3.1", // v1.17.5
"symfony/form": "5.3.*", // v5.3.8
"symfony/framework-bundle": "5.3.*", // v5.3.8
"symfony/monolog-bundle": "^3.0", // v3.7.0
"symfony/property-access": "5.3.*", // v5.3.8
"symfony/property-info": "5.3.*", // v5.3.8
"symfony/rate-limiter": "5.3.*", // v5.3.4
"symfony/runtime": "5.3.*", // v5.3.4
"symfony/security-bundle": "5.3.*", // v5.3.8
"symfony/serializer": "5.3.*", // v5.3.8
"symfony/stopwatch": "5.3.*", // v5.3.4
"symfony/twig-bundle": "5.3.*", // v5.3.4
"symfony/ux-chartjs": "^1.3", // v1.3.0
"symfony/validator": "5.3.*", // v5.3.8
"symfony/webpack-encore-bundle": "^1.7", // v1.12.0
"symfony/yaml": "5.3.*", // v5.3.6
"symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
"twig/extra-bundle": "^2.12|^3.0", // v3.3.3
"twig/string-extra": "^3.3", // v3.3.3
"twig/twig": "^2.12|^3.0" // v3.3.3
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
"symfony/debug-bundle": "5.3.*", // v5.3.4
"symfony/maker-bundle": "^1.15", // v1.34.0
"symfony/var-dumper": "5.3.*", // v5.3.8
"symfony/web-profiler-bundle": "5.3.*", // v5.3.8
"zenstruck/foundry": "^1.1" // v1.13.3
}
}
Error does not added to use Passport
symfony console make:auth
with selection 0 generate Empty authenticator does not generate use Passport by default.