From 066794f922a04d43c5007c1da479efe3f5400936 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Tue, 2 Oct 2018 00:07:55 -0400 Subject: [PATCH 1/5] Overhauling the security section --- _build/redirection_map | 8 + _images/security/http_basic_popup.png | Bin 39500 -> 0 bytes _images/security/symfony_loggedin_wdt.png | Bin 7830 -> 70320 bytes best_practices/security.rst | 20 - controller/error_pages.rst | 7 + doctrine.rst | 52 +- doctrine/registration_form.rst | 15 +- reference/configuration/security.rst | 3 +- reference/configuration/web_profiler.rst | 2 + security.rst | 1149 ++++++----------- security/access_control.rst | 2 + security/acl_advanced.rst | 15 - security/api_key_authentication.rst | 608 --------- ...e_authenticated.rst => auth_providers.rst} | 121 +- security/csrf.rst | 153 +-- security/custom_authentication_provider.rst | 26 +- security/custom_password_authenticator.rst | 227 ---- security/custom_provider.rst | 355 ----- security/entity_provider.rst | 427 +----- security/expressions.rst | 28 + security/firewall_restriction.rst | 4 +- security/force_https.rst | 71 +- security/form_login.rst | 418 +++++- security/form_login_setup.rst | 548 ++++---- security/guard_authentication.rst | 247 +--- security/host_restriction.rst | 5 - security/impersonating_user.rst | 28 +- security/ldap.rst | 2 +- security/multiple_user_providers.rst | 177 --- security/named_encoders.rst | 21 +- security/password_encoding.rst | 45 - security/remember_me.rst | 103 +- security/securing_services.rst | 73 +- security/user_checkers.rst | 5 - security/user_provider.rst | 182 +++ security/voters.rst | 22 +- 36 files changed, 1602 insertions(+), 3567 deletions(-) delete mode 100644 _images/security/http_basic_popup.png delete mode 100644 security/acl_advanced.rst delete mode 100644 security/api_key_authentication.rst rename security/{pre_authenticated.rst => auth_providers.rst} (59%) delete mode 100644 security/custom_password_authenticator.rst delete mode 100644 security/custom_provider.rst delete mode 100644 security/host_restriction.rst delete mode 100644 security/multiple_user_providers.rst delete mode 100644 security/password_encoding.rst create mode 100644 security/user_provider.rst diff --git a/_build/redirection_map b/_build/redirection_map index e0ca0200890..b342cea57df 100644 --- a/_build/redirection_map +++ b/_build/redirection_map @@ -390,3 +390,11 @@ /quick_tour/the_view /quick_tour/flex_recipes /service_container/service_locators /service_container/service_subscribers_locators /templating/overriding /bundles/override +/security/custom_provider /security/user_provider +/security/multiple_user_providers /security/user_provider +/security/custom_password_authenticator /security/guard_authentication +/security/api_key_authentication /security/api_key_authentication +/security/pre_authenticated /security/auth_providers +/security/host_restriction /security/firewall_restriction +/security/acl_advanced /security/acl +/security/password_encoding /security diff --git a/_images/security/http_basic_popup.png b/_images/security/http_basic_popup.png deleted file mode 100644 index fcd9a4ed836ee9633ca19d7a5fdc990e551c3414..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39500 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 S4(;0oe15leI9+4ym-ZjmVq8D~ diff --git a/_images/security/symfony_loggedin_wdt.png b/_images/security/symfony_loggedin_wdt.png index ca182192c9467ee4dd40402be3e7babeb0e3248c..b51e1cafba13a8d407078198430a1c1d31038fe0 100644 GIT binary patch literal 70320 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 diff --git a/best_practices/security.rst b/best_practices/security.rst index a78f8327196..957d8bedfdd 100644 --- a/best_practices/security.rst +++ b/best_practices/security.rst @@ -376,26 +376,6 @@ via the even easier shortcut in a controller:: // ... } -Learn More ----------- - -The `FOSUserBundle`_, developed by the Symfony community, adds support for a -database-backed user system in Symfony. It also handles common tasks like -user registration and forgotten password functionality. - -Enable the :doc:`Remember Me feature ` to -allow your users to stay logged in for a long period of time. - -When providing customer support, sometimes it's necessary to access the application -as some *other* user so that you can reproduce the problem. Symfony provides -the ability to :doc:`impersonate users `. - -If your company uses a user login method not supported by Symfony, you can -develop :doc:`your own user provider ` and -:doc:`your own authentication provider `. - ----- - Next: :doc:`/best_practices/web-assets` .. _`ParamConverter`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 0d2537e6e1f..66e46d16bc2 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -122,6 +122,13 @@ store the HTTP status code and message respectively. for the standard HTML exception page or ``exception.json.twig`` for the JSON exception page. +Security & 404 Pages +-------------------- + +Due to the order of how routing and security are loaded, security information will +*not* be available on your 404 pages. This means that it will appear as if you're +user is logged out on the 404 page (it will work while testing, but not on production). + .. _testing-error-pages: Testing Error Pages during Development diff --git a/doctrine.rst b/doctrine.rst index 493be738bf6..b94daa440eb 100644 --- a/doctrine.rst +++ b/doctrine.rst @@ -228,6 +228,8 @@ This command executes all migration files that have not already been run against your database. You should run this command on production when you deploy to keep your production database up-to-date. +.. _doctrine-add-more-fields: + Migrations & Adding more Fields ------------------------------- @@ -715,12 +717,58 @@ relationships. For info, see :doc:`/doctrine/associations`. +.. _doctrine-fixtures: + Dummy Data Fixtures ------------------- Doctrine provides a library that allows you to programmatically load testing -data into your project (i.e. "fixture data"). For information, see -the "`DoctrineFixturesBundle`_" documentation. +data into your project (i.e. "fixture data"). Install it with: + +.. code-block:: terminal + + $ composer require doctrine/doctrine-fixtures-bundle --dev + +Then, use the ``make:fixtures`` command to generate an empty fixture class: + +.. code-block:: terminal + + $ php bin/console make:fixtures + + The class name of the fixtures to create (e.g. AppFixtures): + > ProductFixture + +Customize the new class to load ``Product`` objects into Doctrine:: + + // src/DataFixtures/ProductFixture.php + namespace App\DataFixtures; + + use Doctrine\Bundle\FixturesBundle\Fixture; + use Doctrine\Common\Persistence\ObjectManager; + + class ProductFixture extends Fixture + { + public function load(ObjectManager $manager) + { + $product = new Product(); + $product->setName('Priceless widget!'); + $product->setPrice(14.50); + $product->setDescription('Ok, I guess it *does* have a price'); + $manager->persist($product); + + // add more products + + $manager->flush(); + } + } + +Empty the database and reload *all* the fixture classes with: + +.. code-block:: terminal + + $ php bin/console doctrine:fixtures:load + +For information, see the "`DoctrineFixturesBundle`_" documentation. Learn more ---------- diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index 8f1c0eed714..054445acce7 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -16,13 +16,8 @@ First, make sure you have all the dependencies you need installed: $ composer require symfony/orm-pack symfony/form symfony/security-bundle symfony/validator -.. tip:: - - The popular `FOSUserBundle`_ provides a registration form, reset password - form and other user management functionality. - If you don't already have a ``User`` entity and a working login system, -first start with :doc:`/security/entity_provider`. +first start by following :doc:`/security`. Your ``User`` entity will probably at least have the following fields: @@ -166,7 +161,7 @@ With some validation added, your class may look something like this:: The :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface` requires a few other methods and your ``security.yaml`` file needs to be configured properly to work with the ``User`` entity. For a more complete example, see -the :ref:`Entity Provider ` article. +the :doc:`Security Guide `. .. _registration-password-max: @@ -420,6 +415,12 @@ To do this, add a ``termsAccepted`` field to your form, but set its The :ref:`constraints ` option is also used, which allows us to add validation, even though there is no ``termsAccepted`` property on ``User``. +Manually Authenticating after Success +------------------------------------- + +If you're using Guard authentication, you can :ref:`automatically authenticate` +after registration is successful. + .. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form .. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle .. _`bcrypt`: https://en.wikipedia.org/wiki/Bcrypt diff --git a/reference/configuration/security.rst b/reference/configuration/security.rst index 822f60ca10f..4e3a85e1662 100644 --- a/reference/configuration/security.rst +++ b/reference/configuration/security.rst @@ -48,8 +48,7 @@ is set to ``true``) when they try to access a protected resource but isn't fully authenticated. This path **must** be accessible by a normal, un-authenticated user, else -you may create a redirect loop. For details, see -":ref:`Avoid Common Pitfalls `". +you may create a redirect loop. check_path .......... diff --git a/reference/configuration/web_profiler.rst b/reference/configuration/web_profiler.rst index f6cf31ca393..1367c794808 100644 --- a/reference/configuration/web_profiler.rst +++ b/reference/configuration/web_profiler.rst @@ -45,6 +45,8 @@ It enables and disables the toolbar entirely. Usually you set this to ``true`` in the ``dev`` and ``test`` environments and to ``false`` in the ``prod`` environment. +.. _intercept_redirects: + intercept_redirects ~~~~~~~~~~~~~~~~~~~ diff --git a/security.rst b/security.rst index 8affa644bdf..f1a32eba9ba 100644 --- a/security.rst +++ b/security.rst @@ -10,28 +10,22 @@ Security Do you prefer video tutorials? Check out the `Symfony Security screencast series`_. Symfony's security system is incredibly powerful, but it can also be confusing -to set up. In this article you'll learn how to set up your application's security -step-by-step, from configuring your firewall and how you load users, to denying -access and fetching the User object. Depending on what you need, sometimes -the initial setup can be tough. But once it's done, Symfony's security system -is both flexible and (hopefully) fun to work with. +to set up. But don't worry! In this article, you'll learn how to set up your app's +security system step-by-step: -Since there's a lot to talk about, this article is organized into a few big -sections: +#. :ref:`Installing security support `; -#. Installing security support; +#. :ref:`Create your User Class `; -#. Initial ``security.yaml`` setup (*authentication*); +#. :ref:`*Authentication* & Firewalls `; -#. Denying access to your app (*authorization*); +#. :ref:`Denying access to your app (*authorization*) `; -#. Fetching the current User object. +#. :ref:`Fetching the current User object `. -These are followed by a number of small (but still captivating) sections, -like :ref:`logging out ` and -:doc:`encoding user passwords `. +A few other important topics are discussed after. -.. _installation: +.. _security-installation: 1) Installation --------------- @@ -43,123 +37,81 @@ install the security feature before using it: $ composer require symfony/security-bundle -.. _security-firewalls: -.. _firewalls-authentication: .. _initial-security-yml-setup-authentication: .. _initial-security-yaml-setup-authentication: +.. _create-user-class: -2) Initial security.yaml Setup (Authentication) ------------------------------------------------ - -The security system is configured in ``config/packages/security.yaml``. The -default configuration looks like this: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - providers: - in_memory: { memory: ~ } - firewalls: - dev: - pattern: ^/(_(profiler|wdt)|css|images|js)/ - security: false - main: - anonymous: ~ - - .. code-block:: xml +2) Create your User Class +------------------------- - - - +No matter *how* you will authenticate (e.g. login form or API tokens) or *where* +your user data will be stored (database, SSO), the next step is always the same: +create a "User" class. The easiest way is to use `MakerBundle`_. - - - - +Let's assume that you want to store your user data in the database with Doctrine: - +.. code-block:: terminal - - - - - + $ php bin/console make:user - .. code-block:: php + The name of the security user class (e.g. User) [User]: + > User - // config/packages/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'in_memory' => array( - 'memory' => null, - ), - ), - 'firewalls' => array( - 'dev' => array( - 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', - 'security' => false, - ), - 'main' => array( - 'anonymous' => null, - ), - ), - )); + Do you want to store user data in the database (via Doctrine)? (yes/no) [yes]: + > yes -The ``firewalls`` key is the *heart* of your security configuration. The -``dev`` firewall isn't important, it just makes sure that Symfony's development -tools - which live under URLs like ``/_profiler`` and ``/_wdt`` aren't blocked -by your security. + Enter a property name that will be the unique "display" name for the user (e.g. + email, username, uuid [email] + > email -.. tip:: + Does this app need to hash/check user passwords? (yes/no) [yes]: + > yes - You can also match a request against other details of the request (e.g. host). For more - information and examples read :doc:`/security/firewall_restriction`. + created: src/Entity/User.php + created: src/Repository/UserRepository.php + updated: src/Entity/User.php + updated: config/packages/security.yaml -All other URLs will be handled by the ``main`` firewall (no ``pattern`` -key means it matches *all* URLs). You can think of the firewall like your -security system, and so it usually makes sense to have just one main firewall. -But this does *not* mean that every URL requires authentication - the ``anonymous`` -key takes care of this. In fact, if you go to the homepage right now, you'll -have access and you'll see that you're "authenticated" as ``anon.``. Don't -be fooled by the "Yes" next to Authenticated, you're just an anonymous user: +That's it! The command asks several questions so that it can generate exactly what +you need. The most important is the ``User.php`` file itself. The *only* rule about +your ``User`` class is that it *must* implement :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. +Feel free to add *any* other fields or logic you need. If your ``User`` class is +an entity (like in this example), you can use the :ref:`make:entity command ` +to add more fields. Also, make sure to make and run a migration for the new entity: -.. image:: /_images/security/anonymous_wdt.png - :align: center +.. code-block:: terminal -You'll learn later how to deny access to certain URLs or controllers. + $ php bin/console make:migration + $ php bin/console doctrine:migrations:migrate -.. note:: +.. _security-user-providers: +.. _where-do-users-come-from-user-providers: - If you do not see toolbar, make sure you installed the :doc:`profiler ` - using this command: +2b) The "User Provider" +----------------------- - .. code-block:: terminal +In addition to your ``User`` class, you also need a "User provider": a class that +helps with a few things, like reloading the User data from the session and some +optional features, like :doc:`remember me ` and +:doc:`impersonation `. - $ composer require --dev symfony/profiler-pack +Fortunately, the ``make:user`` command already configured one for you in your +``security.yaml`` file under the ``providers`` key. -.. tip:: - - Security is *highly* configurable and there's a - :doc:`Security Configuration Reference ` - that shows all of the options with some extra explanation. +If your ``User`` class is an entity, you don't need to do anything else. But if +your class is *not* an entity, then ``make:user`` will also have generated a +``UserProvider`` class that you need to finish. Learn more about user providers +here: :doc:`User Providers `. -A) Configuring how your Users will Authenticate -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _security-encoding-user-password: +.. _encoding-the-user-s-password: -The main job of a firewall is to configure *how* your users will authenticate. -Will they use a login form? HTTP basic authentication? An API token? All of the above? +2c) Encoding Passwords +---------------------- -Let's start with HTTP basic authentication (the old-school prompt) and work up from there. -To activate this, add the ``http_basic`` key under your firewall: +Not all apps have "users" that need passwords. *If* your users have passwords, +you can control how those passwords are encoded in ``security.yaml``. The ``make:user`` +command will pre-configure this for you: .. configuration-block:: @@ -169,11 +121,12 @@ To activate this, add the ``http_basic`` key under your firewall: security: # ... - firewalls: - # ... - main: - anonymous: ~ - http_basic: ~ + encoders: + Symfony\Component\Security\Core\User\User: + # bcrypt or argon21 are recommended + # argon21 is more secure, but requires PHP 7.2 or the Sodium extension + algorithm: bcrypt + cost: 12 .. code-block:: xml @@ -188,87 +141,11 @@ To activate this, add the ``http_basic`` key under your firewall: - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... - 'firewalls' => array( - // ... - 'main' => array( - 'anonymous' => null, - 'http_basic' => null, - ), - ), - )); - -Simple! To try this, you need to require the user to be logged in to see -a page. To make things interesting, create a new page at ``/admin``. For -example, if you use annotations, create something like this:: - - // src/Controller/DefaultController.php - // ... - - use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\Routing\Annotation\Route; - - class DefaultController extends AbstractController - { - /** - * @Route("/admin") - */ - public function admin() - { - return new Response('Admin page!'); - } - } - -Next, add an ``access_control`` entry to ``security.yaml`` that requires the -user to be logged in to access this URL: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - firewalls: - # ... - main: - # ... - - access_control: - # require ROLE_ADMIN for /admin* - - { path: ^/admin, roles: ROLE_ADMIN } - - .. code-block:: xml - - - - + - - - - - - - - @@ -277,215 +154,77 @@ user to be logged in to access this URL: // config/packages/security.php $container->loadFromExtension('security', array( // ... - 'firewalls' => array( - // ... - 'main' => array( - // ... - ), - ), - 'access_control' => array( - // require ROLE_ADMIN for /admin* - array('path' => '^/admin', 'roles' => 'ROLE_ADMIN'), - ), - )); - -.. note:: - - You'll learn more about this ``ROLE_ADMIN`` thing and denying access - later in the :ref:`security-authorization` section. - -Great! Now, if you go to ``/admin``, you'll see the HTTP basic auth prompt: - -.. image:: /_images/security/http_basic_popup.png - :align: center - -But who can you login as? Where do users come from? - -.. _security-form-login: - -.. tip:: - - Want to use a traditional login form? Great! See :doc:`/security/form_login_setup`. - What other methods are supported? See the :doc:`Configuration Reference ` - or :doc:`build your own `. - -.. tip:: - - If your application logs users in via a third-party service such as Google, - Facebook or Twitter, check out the `HWIOAuthBundle`_ community bundle. - -.. _security-user-providers: -.. _where-do-users-come-from-user-providers: - -B) Configuring how Users are Loaded -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When you type in your username, Symfony needs to load that user's information -from somewhere. This is called a "user provider", and you're in charge of -configuring it. Symfony has a built-in way to -:doc:`load users from the database `, -or you can :doc:`create your own user provider `. - -The easiest (but most limited) way, is to configure Symfony to load hardcoded -users directly from the ``security.yaml`` file itself. This is called an "in memory" -provider, but it's better to think of it as an "in configuration" provider: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - providers: - in_memory: - memory: - users: - ryan: - password: ryanpass - roles: 'ROLE_USER' - admin: - password: kitten - roles: 'ROLE_ADMIN' - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - 'providers' => array( - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'ryan' => array( - 'password' => 'ryanpass', - 'roles' => 'ROLE_USER', - ), - 'admin' => array( - 'password' => 'kitten', - 'roles' => 'ROLE_ADMIN', - ), - ), - ), - ), + 'encoders' => array( + 'Symfony\Component\Security\Core\User\User' => array( + 'algorithm' => 'bcrypt', + 'cost' => 12, + ) ), // ... )); -Like with ``firewalls``, you can have multiple ``providers``, but you'll -probably only need one. If you *do* have multiple, you can configure which -*one* provider to use for your firewall under its ``provider`` key (e.g. -``provider: in_memory``). +Now that Symfony knows *how* you want to encode the passwords, you can use the +``UserPasswordEncoderInterface`` service to do this before saving your users to +the database. -.. seealso:: +For example, by using :ref:`DoctrineFixturesBundle `, you can +create dummy database users: - See :doc:`/security/multiple_user_providers` for - all the details about multiple providers setup. - -Try to login using username ``admin`` and password ``kitten``. You should -see an error! - - No encoder has been configured for account "Symfony\\Component\\Security\\Core\\User\\User" - -To fix this, add an ``encoders`` key: - -.. configuration-block:: - - .. code-block:: yaml +.. code-block:: terminal - # config/packages/security.yaml - security: - # ... + $ php bin/console make:fixtures + + The class name of the fixtures to create (e.g. AppFixtures): + > UserFixture - encoders: - Symfony\Component\Security\Core\User\User: plaintext - # ... +Use this service to encode the passwords: - .. code-block:: xml - - - - +.. code-block:: diff - - + // src/DataFixtures/UserFixture.php - - - - + + use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + // ... - .. code-block:: php + class UserFixture extends Fixture + { + + private $passwordEncoder; - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... + + public function __construct(UserPasswordEncoderInterface $passwordEncoder) + + { + + $this->passwordEncoder = $passwordEncoder; + + } - 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => 'plaintext', - ), + public function load(ObjectManager $manager) + { + $user = new User(); // ... - )); -User providers load user information and put it into a ``User`` object. If -you :doc:`load users from the database ` -or :doc:`some other source `, you'll -use your own custom User class. But when you use the "in memory" provider, -it gives you a ``Symfony\Component\Security\Core\User\User`` object. + + $user->setPassword($this->passwordEncoder->encodePassword( + + $user, + + 'the_new_password' + + )); -Whatever your User class is, you need to tell Symfony what algorithm was -used to encode the passwords. In this case, the passwords are just plaintext, -but in a second, you'll change this to use ``bcrypt``. - -If you refresh now, you'll be logged in! The web debug toolbar even tells -you who you are and what roles you have: - -.. image:: /_images/security/symfony_loggedin_wdt.png - :align: center + // ... + } + } -Because this URL requires ``ROLE_ADMIN``, if you had logged in as ``ryan``, -this would deny you access. More on that later (:ref:`security-authorization-access-control`). +Of, you can manually encode a password by running: -Loading Users from the Database -............................... +.. code-block:: terminal -If you'd like to load your users via the Doctrine ORM, that's easy! See -:doc:`/security/entity_provider` for all the details. + $ php bin/console security:encode-password -.. _security-encoding-user-password: -.. _encoding-the-user-s-password: +.. _security-yaml-firewalls: +.. _security-firewalls: +.. _firewalls-authentication: -C) Encoding the User's Password -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +3) Authentication & Firewalls +----------------------------- -Whether your users are stored in ``security.yaml``, in a database or somewhere -else, you'll want to encode their passwords. The most suitable algorithm to use -is ``bcrypt``: +The security system is configured in ``config/packages/security.yaml``. The *most* +important section is ``firewalls``: .. configuration-block:: @@ -493,12 +232,12 @@ is ``bcrypt``: # config/packages/security.yaml security: - # ... - - encoders: - Symfony\Component\Security\Core\User\User: - algorithm: bcrypt - cost: 12 + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + anonymous: ~ .. code-block:: xml @@ -511,13 +250,13 @@ is ``bcrypt``: http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - + - + + + @@ -525,135 +264,94 @@ is ``bcrypt``: // config/packages/security.php $container->loadFromExtension('security', array( - // ... - - 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => array( - 'algorithm' => 'bcrypt', - 'cost' => 12, - ) + 'firewalls' => array( + 'dev' => array( + 'pattern' => '^/(_(profiler|wdt)|css|images|js)/', + 'security' => false, + ), + 'main' => array( + 'anonymous' => null, + ), ), - // ... )); -Of course, your users' passwords now need to be encoded with this exact algorithm. -For hardcoded users, you can use the built-in command: +A "firewall" is your authentication system: the configuration below it defines +*how* your users will be able to authenticate (e.g. login form, API token, etc). -.. code-block:: terminal +Only one firewall is active on each request: Symfony uses the ``pattern`` key +to find the first match (you can also :doc:`match by host or other things `). +The ``dev`` firewall is really a fake firewall: it just makes sure that you don't +accidentally block Symfony's dev tools - which live under URLs like ``/_profiler`` +and ``/_wdt``. - $ php bin/console security:encode-password +All *real* URLs are handled by the ``main`` firewall (no ``pattern`` key means +it matches *all* URLs). But this does *not* mean that every URL requires authentication. +Nope, thanks to the ``anonymous`` key, this firewall *is* accessible anonymously. -It will give you something like this: +In fact, if you go to the homepage right now, you *will* have access and you'll see +that you're "authenticated" as ``anon.``. Don't be fooled by the "Yes" next to +Authenticated, you're just an anonymous user: -.. configuration-block:: - - .. code-block:: yaml +.. image:: /_images/security/anonymous_wdt.png + :align: center - # config/packages/security.yaml - security: - # ... +You'll learn later how to deny access to certain URLs or controllers. - providers: - in_memory: - memory: - users: - ryan: - password: $2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli - roles: 'ROLE_USER' - admin: - password: $2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G - roles: 'ROLE_ADMIN' +.. note:: - .. code-block:: xml + If you do not see toolbar, install the :doc:`profiler ` with: - - - + .. code-block:: terminal - - + $ composer require --dev symfony/profiler-pack - - - - - - - - +Now that we understand our firewall, the next step is to create a way for your +users to authenticate! - .. code-block:: php +.. _security-form-login: - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... +3b) Authenticating your Users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - 'providers' => array( - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'ryan' => array( - 'password' => '$2a$12$LCY0MefVIEc3TYPHV9SNnuzOfyr2p/AXIGoQJEDs4am4JwhNz/jli', - 'roles' => 'ROLE_USER', - ), - 'admin' => array( - 'password' => '$2a$12$cyTWeE9kpq1PjqKFiWUZFuCRPwVyAZwm4XzMZ1qPUFl7/flCM3V0G', - 'roles' => 'ROLE_ADMIN', - ), - ), - ), - ), - ), - // ... - )); +Authentication in Symfony can feel a bit "magic" at first. That's because, instead +of building a route & controller to handle login, you'll activate an +*authentication provider*: some code that runs automatically *before* your controller +is called. -Everything will now work exactly like before. But if you have dynamic users -(e.g. from a database), how can you programmatically encode the password -before inserting them into the database? Don't worry, see -:doc:`/security/password_encoding` for details. +Symfony has several :doc:`built-in authentication providers `. +If your use-case matches one of these *exactly*, great! But, in most cases - including +a login form - *we recommend building a Guard Authenticator*: a class that allows +you to control *every* part of the authentication process (see the next section). .. tip:: - Supported algorithms for this method depend on your PHP version, but - include the algorithms returned by the PHP function :phpfunction:`hash_algos` - as well as a few others (e.g. bcrypt and argon2i). See the ``encoders`` key - in the :doc:`Security Reference Section ` - for examples. - - It's also possible to use different hashing algorithms on a user-by-user - basis. See :doc:`/security/named_encoders` for more details. - -D) Configuration Done! -~~~~~~~~~~~~~~~~~~~~~~ - -Congratulations! You now have a working authentication system that uses HTTP -basic auth and loads users right from the ``security.yaml`` file. + If your application logs users in via a third-party service such as Google, + Facebook or Twitter (social login), check out the `HWIOAuthBundle`_ community + bundle. -Your next steps depend on your setup: +Guard Authenticators +.................... -* Configure a different way for your users to login, like a :ref:`login form ` - or :doc:`something completely custom `; +A Guard authenticator is a class that gives you *complete* control over your +authentication process. There are *many* different ways to build an authenticator, +so here are a few common use-cases: -* Load users from a different source, like the :doc:`database ` - or :doc:`some other source `; +* :doc:`/security/form_login_setup` +* :doc:`/security/guard_authentication` -* Learn how to deny access, load the User object and deal with roles in the - :ref:`Authorization ` section. +For the most detailed description of authenticators and how they work, see +:doc:`/security/guard_authentication`. .. _`security-authorization`: .. _denying-access-roles-and-other-authorization: -3) Denying Access, Roles and other Authorization +4) Denying Access, Roles and other Authorization ------------------------------------------------ -Users can now login to your app using ``http_basic`` or some other method. -Great! Now, you need to learn how to deny access and work with the User object. -This is called **authorization**, and its job is to decide if a user can -access some resource (a URL, a model object, a method call, ...). +Users can now login to your app using your login form. Great! Now, you need to learn +how to deny access and work with the User object. This is called **authorization**, +and its job is to decide if a user can access some resource (a URL, a model object, +a method call, ...). The process of authorization has two different sides: @@ -662,43 +360,44 @@ The process of authorization has two different sides: "attribute" (most commonly a role like ``ROLE_ADMIN``) in order to be accessed. -.. tip:: - - In addition to roles (e.g. ``ROLE_ADMIN``), you can protect a resource - using other attributes/strings (e.g. ``EDIT``) and use voters to give these - meaning. This might come in handy if you need to check if user A can "EDIT" - some object B (e.g. a Product with id 5). See :ref:`security-secure-objects`. - Roles ~~~~~ -When a user logs in, they receive a set of roles (e.g. ``ROLE_ADMIN``). In -the example above, these are hardcoded into ``security.yaml``. If you're -loading users from the database, these are probably stored on a column -in your table. +When a user logs in, Symfony calls the ``getRoles()`` method on your ``User`` +object to determime which roles this user has. In the ``User`` class that we +generated earlier, the roles are an array that's stored in the database, and +every user is *always* given at least one role: ``ROLE_USER``:: + + // src/Entity/User.php + // ... + + /** + * @ORM\Column(type="json") + */ + private $roles = []; + + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; -.. caution:: + return array_unique($roles); + } - All roles you assign to a user **must** begin with the ``ROLE_`` prefix. - Otherwise, they won't be handled by Symfony's security system in the - normal way (i.e. unless you're doing something advanced, assigning a - role like ``FOO`` to a user and then checking for ``FOO`` as described - :ref:`below ` will not work). +This is a nice default, but you can do *whatever* you want to determine which roles +a user should have. Here are a few guidelines: -Roles are simple, and are basically strings that you invent and use as needed. -For example, if you need to start limiting access to the blog admin section -of your website, you could protect that section using a ``ROLE_BLOG_ADMIN`` -role. This role doesn't need to be defined anywhere - you can just start using -it. +* Every role **must start with** ``ROLE_`` (otherwise, things won't as expected) -.. tip:: +* Other than the above rule, a role is just a string and you can invent what you + need (e.g. ``ROLE_PRODUCT_ADMIN``) - Make sure every user has at least *one* role, or your user will look - like they're not authenticated. A common convention is to give *every* - user ``ROLE_USER``. +* Every User **must** have at least **one** role - a common convention is to give + *every* user ``ROLE_USER``. -You can also specify a :ref:`role hierarchy ` where -some roles automatically mean that you also have other roles. +You can also use a :ref:`role hierarchy ` where having +some roles automatically gives you other roles. .. _security-role-authorization: @@ -711,16 +410,16 @@ There are **two** ways to deny access to something: allows you to protect URL patterns (e.g. ``/admin/*``). This is easy, but less flexible; -#. :ref:`in your code via the security.authorization_checker service `. +#. :ref:`in your controller (or other code) `. .. _security-authorization-access-control: Securing URL patterns (access_control) ...................................... -The most basic way to secure part of your application is to secure an entire -URL pattern. You saw this earlier, where anything matching the regular expression -``^/admin`` requires the ``ROLE_ADMIN`` role: +The most basic way to secure part of your app is to secure an entire URL pattern +in ``security.yaml``. For example, to require ``ROLE_ADMIN`` for all URLs that +start with ``/admin``, you can: .. configuration-block:: @@ -779,14 +478,9 @@ URL pattern. You saw this earlier, where anything matching the regular expressio ), )); -This is great for securing entire sections, but you'll also probably want -to :ref:`secure your individual controllers ` -as well. - You can define as many URL patterns as you need - each is a regular expression. -**BUT**, only **one** will be matched. Symfony will look at each starting -at the top, and stop as soon as it finds one ``access_control`` entry that -matches the URL. +**BUT**, only **one** will be matched per request: Symfony starts at the top of +the list and stops when it finds the first match: .. configuration-block:: @@ -797,7 +491,10 @@ matches the URL. # ... access_control: + # matches /admin/users/* - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN } + + # matches /admin/* except for anything matching the above rule - { path: ^/admin, roles: ROLE_ADMIN } .. code-block:: xml @@ -834,17 +531,9 @@ Prepending the path with ``^`` means that only URLs *beginning* with the pattern are matched. For example, a path of simply ``/admin`` (without the ``^``) would match ``/admin/foo`` but would also match URLs like ``/foo/admin``. -.. _security-access-control-explanation: - -.. sidebar:: Understanding how ``access_control`` Works - - The ``access_control`` section is very powerful, but it can also be dangerous - (because it involves security) if you don't understand *how* it works. - In addition to the URL, the ``access_control`` can match on IP address, - host name and HTTP methods. It can also be used to redirect a user to - the ``https`` version of a URL pattern. - - To learn about all of this, see :doc:`/security/access_control`. +Each ``access_control`` can also match on IP address, host name and HTTP methods. +It can also be used to redirect a user to the ``https`` version of a URL pattern. +See :doc:`/security/access_control`. .. _security-securing-controller: @@ -853,59 +542,57 @@ Securing Controllers and other Code You can easily deny access from inside a controller:: + // src/Controller/AdminController.php // ... - public function hello($name) + public function adminDashboard() { - // The second parameter is used to specify on what object the role is tested. - $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!'); + $this->denyAccessUnlessGranted('ROLE_ADMIN'); - // ... + // or add an optional message - seen by developers + $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!'); } -.. tip:: - - The ``denyAccessUnlessGranted()`` is a shortcut provided by the optional - :ref:`base controller provided by Symfony `. - It's equivalent to the following code:: - - use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - // ... - - public function hello($name, AuthorizationCheckerInterface $authChecker) - { - if (false === $authChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException('Unable to access this page!'); - } - - // ... - } - -If access is not granted, a special +That's it! If access is not granted, a special :class:`Symfony\\Component\\Security\\Core\\Exception\\AccessDeniedException` -is thrown, which ultimately triggers a 403 HTTP response inside Symfony. +is thrown and no more code in your controller is executed. Then, one of two things +will happen: + +1) If the user isn't logged in yet, they will be asked to login (e.g. redirected + to the login page). -That's it! If the user isn't logged in yet, they will be asked to login (e.g. -redirected to the login page). If they *are* logged in, but do *not* have the -``ROLE_ADMIN`` role, they'll be shown the 403 access denied page (which you can -:ref:`customize `). If they are logged in -and have the correct roles, the code will be executed. +2) If the user *is* logged in, but does *not* have the ``ROLE_ADMIN`` role, they'll + be shown the 403 access denied page (which you can + :ref:`customize `). .. _security-securing-controller-annotations: Thanks to the SensioFrameworkExtraBundle, you can also secure your controller -using annotations:: +using annotations: +.. code-block:: diff + + // src/Controller/AdminController.php // ... - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - /** - * @Security("has_role('ROLE_ADMIN')") - */ - public function hello($name) + + use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; + + + /** + + * Require ROLE_ADMIN for *every* controller method in this class. + + * + + * @IsGranted("ROLE_ADMIN") + + */ + class AdminController extends AbstractController { - // ... + + /** + + * Require ROLE_ADMIN for only this controller method. + + * + + * @IsGranted("ROLE_ADMIN") + + */ + public function adminDashboard() + { + // ... + } } For more information, see the `FrameworkExtraBundle documentation`_. @@ -915,7 +602,7 @@ For more information, see the `FrameworkExtraBundle documentation`_. Access Control in Templates ........................... -If you want to check if the current user has a role inside a template, use +If you want to check if the current access inside a template, use the built-in ``is_granted()`` helper function: .. code-block:: html+twig @@ -927,37 +614,31 @@ the built-in ``is_granted()`` helper function: Securing other Services ....................... -Anything in Symfony can be protected by doing something similar to the code -used to secure a controller. For example, suppose you have a service (i.e. a -PHP class) whose job is to send emails. You can restrict use of this class - no -matter where it's being used from - to only certain users. - -For more information see :doc:`/security/securing_services`. +See :doc:`/security/securing_services`. Checking to see if a User is Logged In (IS_AUTHENTICATED_FULLY) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -So far, you've checked access based on roles - those strings that start with -``ROLE_`` and are assigned to users. But if you *only* want to check if a -user is logged in (you don't care about roles), then you can use -``IS_AUTHENTICATED_FULLY``:: +If you *only* want to check if a user is simply logged in (you don't care about roles), +you have two options. First, if you've given *every* user ``ROLE_USER``, you can +just check for that role. Otherwise, you can use a special "attribute" in place +of a role:: // ... - public function hello($name) + public function adminDashboard() { $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); // ... } -.. tip:: - - You can of course also use this in ``access_control``. +You can use ``IS_AUTHENTICATED_FULLY`` anywhere roles are used: like ``access_control`` +or in Twig. ``IS_AUTHENTICATED_FULLY`` isn't a role, but it kind of acts like one, and every -user that has successfully logged in will have this. In fact, there are three -special attributes like this: +user that has logged in will have this. ACtually, there are 3 special attributes +like this: * ``IS_AUTHENTICATED_REMEMBERED``: *All* logged in users have this, even if they are logged in because of a "remember me cookie". Even if you don't @@ -972,30 +653,6 @@ special attributes like this: this - this is useful when *whitelisting* URLs to guarantee access - some details are in :doc:`/security/access_control`. -.. _security-template-expression: - -You can also use expressions inside your templates: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% if is_granted(expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - )) %} - Delete - {% endif %} - - .. code-block:: html+php - - isGranted(new Expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - ))): ?> - Delete - - -For more details on expressions and security, see :doc:`/security/expressions`. - .. _security-secure-objects: Access Control Lists (ACLs): Securing individual Database Objects @@ -1003,102 +660,73 @@ Access Control Lists (ACLs): Securing individual Database Objects Imagine you are designing a blog where users can comment on your posts. You also want a user to be able to edit their own comments, but not those of -other users. Also, as the admin user, you yourself want to be able to edit -*all* comments. +other users. Also, as the admin user, you want to be able to edit *all* comments. -:doc:`Voters ` allow you to write own business logic (e.g. the -user can edit this post because they were the creator) to determine access. -That's why voters are officially recommended by Symfony to create ACL-like +:doc:`Voters ` allow you to write *whatever* business logic you +need (e.g. the user can edit this post because they are the creator) to determine +access. That's why voters are officially recommended by Symfony to create ACL-like security systems. If you still prefer to use traditional ACLs, refer to the `Symfony ACL bundle`_. .. _retrieving-the-user-object: -4) Retrieving the User Object ------------------------------ +5) Fetching the User Object +--------------------------- After authentication, the ``User`` object of the current user can be accessed -via the ``getUser()`` shortcut (which uses the ``security.token_storage`` -service). From inside a controller, this will look like:: +via the ``getUser()`` shortcut:: public function index() { + // usually you'll want to make sure the user is authenticated first $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + // returns your User object, or null if the user is not authenticated $user = $this->getUser(); - } - -.. tip:: - - The user will be an object and the class of that object will depend on - your :ref:`user provider `. - -Now you can call whatever methods are on *your* User object. For example, -if your User object has a ``getFirstName()`` method, you could use that:: - - use Symfony\Component\HttpFoundation\Response; - // ... - - public function index() - { - // ... + // Call whatever methods you've added to your User class + // For example, if you added a getFirstName() method, you can use that. return new Response('Well hi there '.$user->getFirstName()); } -Always Check if the User is Logged In -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -It's important to check if the user is authenticated first. If they're not, -``$user`` will either be ``null`` or the string ``anon.``. Wait, what? Yes, -this is a quirk. If you're not logged in, the user is technically the string -``anon.``, though the ``getUser()`` controller shortcut converts this to -``null`` for convenience. - -The point is this: always check to see if the user is logged in before using -the User object, and use the ``isGranted()`` method (or -:ref:`access_control `) to do this:: - - // yay! Use this to see if the user is logged in - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - - // boo :(. Never check for the User object to see if they're logged in - if ($this->getUser()) { - // ... - } +5B) Fetching the User from a Service +------------------------------------ -.. note:: +If you need to get the logged in user from a service, use the +:class:`Symfony\\Component\\Security\\Core\\Security` service:: - An alternative way to get the current user in a controller is to type-hint - the controller argument with - :class:`Symfony\\Component\\Security\\Core\\Security`:: + // src/Service/ExampleService.php + // ... - use Symfony\Component\Security\Core\Security; + use Symfony\\Component\\Security\\Core\\Security; + + class ExampleService + { + private $security; - public function indexAction(Security $security) + public function __construct(Security $security) { - $user = $security->getUser(); + $this->security = $security; } - .. versionadded:: 3.4 - The ``Security`` utility class was introduced in Symfony 3.4. - - This is only recommended for experienced developers who don't extend from the - :ref:`Symfony base controller ` and - don't use the :class:`Symfony\\Bundle\\FrameworkBundle\\Controller\\ControllerTrait` - either. Otherwise, it's recommended to keep using the ``getUser()`` shortcut. + public function someMethod() + { + // returns User object or null if not authenticated + $user = $this->security->getUser(); + } + } -Retrieving the User in a Template -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Fetch the User in a Template +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In a Twig Template this object can be accessed via the :ref:`app.user ` +In a Twig Template the user object can be accessed via the :ref:`app.user ` key: .. code-block:: html+twig {% if is_granted('IS_AUTHENTICATED_FULLY') %} -

Username: {{ app.user.username }}

+

Email: {{ app.user.email }}

{% endif %} .. _security-logging-out: @@ -1106,17 +734,7 @@ key: Logging Out ----------- -.. caution:: - - Notice that when using http-basic authenticated firewalls, there is no - real way to log out : the only way to *log out* is to have the browser - stop sending your name and password on every request. Clearing your - browser cache or restarting your browser usually helps. Some web developer - tools might be helpful here too. - -Usually, you'll also want your users to be able to log out. Fortunately, -the firewall can handle this automatically for you when you activate the -``logout`` config parameter: +To add logout, activate the ``logout`` config parameter under your firewall: .. configuration-block:: @@ -1127,11 +745,13 @@ the firewall can handle this automatically for you when you activate the # ... firewalls: - secured_area: + main: # ... logout: - path: /logout - target: / + path: app_logout + + # where to redirect after logout + # target: app_any_route .. code-block:: xml @@ -1148,7 +768,7 @@ the firewall can handle this automatically for you when you activate the - + @@ -1162,7 +782,7 @@ the firewall can handle this automatically for you when you activate the 'firewalls' => array( 'secured_area' => array( // ... - 'logout' => array('path' => '/logout', 'target' => '/'), + 'logout' => array('path' => 'app_logout'), ), ), )); @@ -1174,9 +794,28 @@ Next, you'll need to create a route for this URL (but not a controller): .. code-block:: yaml # config/routes.yaml - logout: + app_logout: path: /logout + .. code-block:: php-annotations + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="app_logout") + */ + public function logout() + { + // controller should be blank - will never be executed + } + } + .. code-block:: xml @@ -1186,7 +825,7 @@ Next, you'll need to create a route for this URL (but not a controller): xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd"> - + .. code-block:: php @@ -1196,30 +835,25 @@ Next, you'll need to create a route for this URL (but not a controller): use Symfony\Component\Routing\Route; $routes = new RouteCollection(); - $routes->add('logout', new Route('/logout')); + $routes->add('app_logout', new Route('/logout')); return $routes; -And that's it! By sending a user to ``/logout`` (or whatever you configure -the ``path`` to be), Symfony will un-authenticate the current user. - -Once the user has been logged out, they will be redirected to whatever path -is defined by the ``target`` parameter above (e.g. the ``homepage``). +And that's it! By sending a user to the ``app_logout`` route (i.e. to ``/logout``) +Symfony will un-authenticate the current user and redirect them. .. tip:: - If you need to do something more interesting after logging out, you can - specify a logout success handler by adding a ``success_handler`` key - and pointing it to a service id of a class that implements + Need more control of what happens after logout? Add a ``success_handler`` key + under ``logout`` and point it to a service id of a class that implements :class:`Symfony\\Component\\Security\\Http\\Logout\\LogoutSuccessHandlerInterface`. - See :doc:`Security Configuration Reference `. .. _security-role-hierarchy: Hierarchical Roles ------------------ -Instead of associating many roles to users, you can define role inheritance +Instead of giving many roles to each user, you can define role inheritance rules by creating a role hierarchy: .. configuration-block:: @@ -1267,31 +901,52 @@ rules by creating a role hierarchy: ), )); -In the above configuration, users with ``ROLE_ADMIN`` role will also have the -``ROLE_USER`` role. The ``ROLE_SUPER_ADMIN`` role has ``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` -and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). +Users with the ``ROLE_ADMIN`` role will also have the +``ROLE_USER`` role. And users with ``ROLE_SUPER_ADMIN``, will automatically have +``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). .. note:: - The value of the ``role_hierarchy`` option is defined statically, so you - can't for example store the role hierarchy in a database. If you need that, - create a custom :doc:`security voter ` that looks for the - user roles in the database. - -Final Words ------------ - -Woh! Nice work! You now know more than the basics of security. The hardest -parts are when you have custom requirements: like a custom authentication -strategy (e.g. API tokens), complex authorization logic and many other things -(because security is complex!). - -Fortunately, there are a lot of articles aimed at describing many of these -situations. Also, see the :doc:`Security Reference Section `. -Many of the options don't have specific details, but seeing the full possible -configuration tree may be useful. - -Good luck! + The ``role_hierarchy`` values iare static - you can't, for example, store the + role hierarchy in a database. If you need that, create a custom + :doc:`security voter ` that looks for the user roles + in the database. + +Checking for Security Vulnerabilities in your Dependences +--------------------------------------------------------- + +See :doc:`/security/security_checker`. + +Frequently Asked Questions +-------------------------- + +**Can I have Multiple Firewalls?** + Yes! But it's usually not necessary. Each firewall is like a separate security + system. And so, unless you have *very* different authentication needs, one + firewall usually works well. + +**Can I Share Authentication Between Firewalls?** + Yes, but only with some configuration. If you're using multiple firewalls and + you authenticate against one firewall, you will *not* be authenticated against + any other firewalls automatically. Different firewalls are like different security + systems. To do this you have to explicitly specify the same + :ref:`reference-security-firewall-context` for different firewalls. But usually + for most applications, having one main firewall is enough. + +**Security doesn't seem to work on my Error Pages** + As routing is done *before* security, 404 error pages are not covered by + any firewall. This means you can't check for security or even access the + user object on these pages. See :doc:`/controller/error_pages` + for more details. + +**My Authentication Doesn't Seem to Work: No Errors, but I'm Never Logged In** + Sometimes authentication may be successful, but after redirecting, you're + logged out immediately due to a problem loading the ``User`` from the session. + To see if this is the issue, temporarily enable :ref:`intercept_redirects`. + Then, when you login, instead of being redirected, you'll be stopped. Check + the web debug toolbar on that page to see if you're logged in. If you *are*, + but are no longer logged in after redirecting, then there is a problem loading + your User from the session. See :ref:`user_session_refresh`. Learn More ---------- @@ -1303,24 +958,18 @@ Authentication (Identifying/Logging in the User) :maxdepth: 1 security/form_login_setup - security/ldap - security/entity_provider security/guard_authentication + security/auth_providers + security/user_provider + security/ldap security/remember_me security/impersonating_user - security/form_login - security/custom_provider - security/custom_password_authenticator - security/api_key_authentication - security/custom_authentication_provider - security/pre_authenticated - security/csrf + security/user_checkers security/named_encoders - security/multiple_user_providers security/multiple_guard_authenticators security/firewall_restriction - security/host_restriction - security/user_checkers + security/csrf + security/custom_authentication_provider Authorization (Denying Access) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1329,23 +978,15 @@ Authorization (Denying Access) :maxdepth: 1 security/voters - security/acl - security/acl_advanced - security/force_https security/securing_services security/access_control security/access_denied_handler - -Other Security Related Topics -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. toctree:: - :maxdepth: 1 - - security/password_encoding + security/acl + security/force_https security/security_checker .. _`frameworkextrabundle documentation`: https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html .. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle .. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle .. _`Symfony Security screencast series`: https://symfonycasts.com/screencast/symfony-security +.. _`MakerBundle`: https://github.com/symfony/maker-bundle diff --git a/security/access_control.rst b/security/access_control.rst index 7c60ac7f8c1..eeab4457c06 100644 --- a/security/access_control.rst +++ b/security/access_control.rst @@ -1,3 +1,5 @@ +.. _security-access-control-explanation: + How Does the Security access_control Work? ========================================== diff --git a/security/acl_advanced.rst b/security/acl_advanced.rst deleted file mode 100644 index ed4b5a5c07a..00000000000 --- a/security/acl_advanced.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. index:: - single: Security; Advanced ACL concepts - -How to Use advanced ACL Concepts -================================ - -.. caution:: - - ACL support was removed in Symfony 4.0. Install the `Symfony ACL bundle`_ - and refer to its documentation if you want to keep using ACL. - - Consider using :doc:`security voters `, - the alternative to ACLs recommended by Symfony. - -.. _`Symfony ACL bundle`: https://github.com/symfony/acl-bundle diff --git a/security/api_key_authentication.rst b/security/api_key_authentication.rst deleted file mode 100644 index 9e288dfffe5..00000000000 --- a/security/api_key_authentication.rst +++ /dev/null @@ -1,608 +0,0 @@ -.. index:: - single: Security; Custom Request Authenticator - -How to Authenticate Users with API Keys -======================================= - -.. tip:: - - Check out :doc:`/security/guard_authentication` for a simpler and more - flexible way to accomplish custom authentication tasks like this. - -Nowadays, it's quite usual to authenticate the user via an API key (when developing -a web service for instance). The API key is provided for every request and is -passed as a query string parameter or via an HTTP header. - -The API Key Authenticator -------------------------- - -Authenticating a user based on the Request information should be done via a -pre-authentication mechanism. The :class:`Symfony\\Component\\Security\\Http\\Authentication\\SimplePreAuthenticatorInterface` -allows you to implement such a scheme really easily. - -Your exact situation may differ, but in this example, a token is read -from an ``apikey`` query parameter, the proper username is loaded from that -value and then a User object is created:: - - // src/Security/ApiKeyAuthenticator.php - namespace App\Security; - - use App\Security\ApiKeyUserProvider; - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - public function createToken(Request $request, $providerKey) - { - // look for an apikey query parameter - $apiKey = $request->query->get('apikey'); - - // or if you want to use an "apikey" header, then do something like this: - // $apiKey = $request->headers->get('apikey'); - - if (!$apiKey) { - throw new BadCredentialsException(); - - // or to just skip api key authentication - // return null; - } - - return new PreAuthenticatedToken( - 'anon.', - $apiKey, - $providerKey - ); - } - - public function supportsToken(TokenInterface $token, $providerKey) - { - return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; - } - - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - if (!$userProvider instanceof ApiKeyUserProvider) { - throw new \InvalidArgumentException( - sprintf( - 'The user provider must be an instance of ApiKeyUserProvider (%s was given).', - get_class($userProvider) - ) - ); - } - - $apiKey = $token->getCredentials(); - $username = $userProvider->getUsernameForApiKey($apiKey); - - if (!$username) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException( - sprintf('API Key "%s" does not exist.', $apiKey) - ); - } - - $user = $userProvider->loadUserByUsername($username); - - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - } - -Once you've :ref:`configured ` everything, -you'll be able to authenticate by adding an apikey parameter to the query -string, like ``http://example.com/api/foo?apikey=37b51d194a7513e45b56f6524f2d51f2``. - -The authentication process has several steps, and your implementation will -probably differ: - -1. createToken -~~~~~~~~~~~~~~ - -Early in the request cycle, Symfony calls ``createToken()``. Your job here -is to create a token object that contains all of the information from the -request that you need to authenticate the user (e.g. the ``apikey`` query -parameter). If that information is missing, throwing a -:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` -will cause authentication to fail. You might want to return ``null`` instead -to just skip the authentication, so Symfony can fallback to another authentication -method, if any. - -.. caution:: - - In case you return ``null`` from your ``createToken()`` method, Symfony - passes this request to the next authentication provider. If you haven't - configured any other provider, enable the ``anonymous`` option in your - firewall. This way Symfony executes the anonymous authentication provider - and you'll get an ``AnonymousToken``. - -2. supportsToken -~~~~~~~~~~~~~~~~ - -.. include:: _supportsToken.rst.inc - -3. authenticateToken -~~~~~~~~~~~~~~~~~~~~ - -If ``supportsToken()`` returns ``true``, Symfony will now call ``authenticateToken()``. -One key part is the ``$userProvider``, which is an external class that helps -you load information about the user. You'll learn more about this next. - -In this specific example, the following things happen in ``authenticateToken()``: - -#. First, you use the ``$userProvider`` to somehow look up the ``$username`` that - corresponds to the ``$apiKey``; -#. Second, you use the ``$userProvider`` again to load or create a ``User`` - object for the ``$username``; -#. Finally, you create an *authenticated token* (i.e. a token with at least one - role) that has the proper roles and the User object attached to it. - -The goal is ultimately to use the ``$apiKey`` to find or create a ``User`` -object. *How* you do this (e.g. query a database) and the exact class for -your ``User`` object may vary. Those differences will be most obvious in your -user provider. - -The User Provider -~~~~~~~~~~~~~~~~~ - -The ``$userProvider`` can be any user provider (see :doc:`/security/custom_provider`). -In this example, the ``$apiKey`` is used to somehow find the username for -the user. This work is done in a ``getUsernameForApiKey()`` method, which -is created entirely custom for this use-case (i.e. this isn't a method that's -used by Symfony's core user provider system). - -The ``$userProvider`` might look something like this:: - - // src/Security/ApiKeyUserProvider.php - namespace App\Security; - - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Core\User\User; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - - class ApiKeyUserProvider implements UserProviderInterface - { - public function getUsernameForApiKey($apiKey) - { - // Look up the username based on the token in the database, via - // an API call, or do something entirely different - $username = ...; - - return $username; - } - - public function loadUserByUsername($username) - { - return new User( - $username, - null, - // the roles for the user - you may choose to determine - // these dynamically somehow based on the user - array('ROLE_API') - ); - } - - public function refreshUser(UserInterface $user) - { - // this is used for storing authentication in the session - // but in this example, the token is sent in each request, - // so authentication can be stateless. Throwing this exception - // is proper to make things stateless - throw new UnsupportedUserException(); - } - - public function supportsClass($class) - { - return User::class === $class; - } - } - -Next, make sure this class is registered as a service. If you're using the -:ref:`default services.yaml configuration `, -that happens automatically. A little later, you'll reference this service in -your :ref:`security.yaml configuration `. - -.. note:: - - Read the dedicated article to learn - :doc:`how to create a custom user provider `. - -The logic inside ``getUsernameForApiKey()`` is up to you. You may somehow transform -the API key (e.g. ``37b51d``) into a username (e.g. ``jondoe``) by looking -up some information in a "token" database table. - -The same is true for ``loadUserByUsername()``. In this example, Symfony's core -:class:`Symfony\\Component\\Security\\Core\\User\\User` class is simply created. -This makes sense if you don't need to store any extra information on your -User object (e.g. ``firstName``). But if you do, you may instead have your *own* -user class which you create and populate here by querying a database. This -would allow you to have custom data on the ``User`` object. - -Finally, just make sure that ``supportsClass()`` returns ``true`` for User -objects with the same class as whatever user you return in ``loadUserByUsername()``. - -If your authentication is stateless like in this example (i.e. you expect -the user to send the API key with every request and so you don't save the -login to the session), then you can simply throw the ``UnsupportedUserException`` -exception in ``refreshUser()``. - -.. note:: - - If you *do* want to store authentication data in the session so that - the key doesn't need to be sent on every request, see :ref:`security-api-key-session`. - -Handling Authentication Failure -------------------------------- - -In order for your ``ApiKeyAuthenticator`` to correctly display a 401 -http status when either bad credentials or authentication fails you will -need to implement the :class:`Symfony\\Component\\Security\\Http\\Authentication\\AuthenticationFailureHandlerInterface` on your -Authenticator. This will provide a method ``onAuthenticationFailure()`` which -you can use to create an error ``Response``:: - - // src/Security/ApiKeyAuthenticator.php - namespace App\Security; - - use Symfony\Component\Security\Core\Exception\AuthenticationException; - use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; - use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface; - use Symfony\Component\HttpFoundation\Response; - use Symfony\Component\HttpFoundation\Request; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface - { - // ... - - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) - { - return new Response( - // this contains information about *why* authentication failed - // use it, or return your own message - strtr($exception->getMessageKey(), $exception->getMessageData()), - 401 - ); - } - } - -.. _security-api-key-config: - -Configuration -------------- - -Once you have your ``ApiKeyAuthenticator`` all setup, you need to register -it as a service. If you're using the :ref:`default services.yaml configuration `, -that happens automatically. - -The last step is to activate your authenticator and custom user provider in the -``firewalls`` section of your security configuration using the ``simple_preauth`` -and ``provider`` keys: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - providers: - api_key_user_provider: - id: App\Security\ApiKeyUserProvider - - firewalls: - main: - pattern: ^/api - stateless: true - simple_preauth: - authenticator: App\Security\ApiKeyAuthenticator - provider: api_key_user_provider - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - - // ... - use App\Security\ApiKeyAuthenticator; - use App\Security\ApiKeyUserProvider; - - $container->loadFromExtension('security', array( - 'providers' => array( - 'api_key_user_provider' => array( - 'id' => ApiKeyUserProvider::class, - ), - ), - 'firewalls' => array( - 'main' => array( - 'pattern' => '^/api', - 'stateless' => true, - 'simple_preauth' => array( - 'authenticator' => ApiKeyAuthenticator::class, - ), - 'provider' => 'api_key_user_provider', - ), - ), - )); - -If you have defined ``access_control``, make sure to add a new entry: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - access_control: - - { path: ^/api, roles: ROLE_API } - - .. code-block:: xml - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - 'access_control' => array( - array( - 'path' => '^/api', - 'role' => 'ROLE_API', - ), - ), - )); - -That's it! Now, your ``ApiKeyAuthenticator`` should be called at the beginning -of each request and your authentication process will take place. - -The ``stateless`` configuration parameter prevents Symfony from trying to -store the authentication information in the session, which isn't necessary -since the client will send the ``apikey`` on each request. If you *do* need -to store authentication in the session, keep reading! - -.. _security-api-key-session: - -Storing Authentication in the Session -------------------------------------- - -So far, this article has described a situation where some sort of authentication -token is sent on every request. But in some situations (like an OAuth flow), -the token may be sent on only *one* request. In this case, you will want to -authenticate the user and store that authentication in the session so that -the user is automatically logged in for every subsequent request. - -To make this work, first remove the ``stateless`` key from your firewall -configuration or set it to ``false``: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - pattern: ^/api - stateless: false - # ... - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - - // .. - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/api', - 'stateless' => false, - // ... - ), - ), - )); - -Even though the token is being stored in the session, the credentials - in this -case the API key (i.e. ``$token->getCredentials()``) - are not stored in the session -for security reasons. To take advantage of the session, update ``ApiKeyAuthenticator`` -to see if the stored token has a valid User object that can be used:: - - // src/Security/ApiKeyAuthenticator.php - - // ... - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - // ... - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - if (!$userProvider instanceof ApiKeyUserProvider) { - throw new \InvalidArgumentException( - sprintf( - 'The user provider must be an instance of ApiKeyUserProvider (%s was given).', - get_class($userProvider) - ) - ); - } - - $apiKey = $token->getCredentials(); - $username = $userProvider->getUsernameForApiKey($apiKey); - - // User is the Entity which represents your user - $user = $token->getUser(); - if ($user instanceof User) { - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - - if (!$username) { - // this message will be returned to the client - throw new CustomUserMessageAuthenticationException( - sprintf('API Key "%s" does not exist.', $apiKey) - ); - } - - $user = $userProvider->loadUserByUsername($username); - - return new PreAuthenticatedToken( - $user, - $apiKey, - $providerKey, - $user->getRoles() - ); - } - // ... - } - -Storing authentication information in the session works like this: - -#. At the end of each request, Symfony serializes the token object (returned - from ``authenticateToken()``), which also serializes the ``User`` object - (since it's set on a property on the token); -#. On the next request the token is deserialized and the deserialized ``User`` - object is passed to the ``refreshUser()`` function of the user provider. - -The second step is the important one: Symfony calls ``refreshUser()`` and passes -you the user object that was serialized in the session. If your users are -stored in the database, then you may want to re-query for a fresh version -of the user to make sure it's not out-of-date. But regardless of your requirements, -``refreshUser()`` should now return the User object:: - - // src/Security/ApiKeyUserProvider.php - - // ... - class ApiKeyUserProvider implements UserProviderInterface - { - // ... - - public function refreshUser(UserInterface $user) - { - // $user is the User that you set in the token inside authenticateToken() - // after it has been deserialized from the session - - // you might use $user to query the database for a fresh user - // $id = $user->getId(); - // use $id to make a query - - // if you are *not* reading from a database and are just creating - // a User object (like in this example), you can just return it - return $user; - } - } - -.. note:: - - You'll also want to make sure that your ``User`` object is being serialized - correctly. If your ``User`` object has private properties, PHP can't serialize - those. In this case, you may get back a User object that has a ``null`` - value for each property. For an example, see :doc:`/security/entity_provider`. - -Only Authenticating for Certain URLs ------------------------------------- - -This article has assumed that you want to look for the ``apikey`` authentication -on *every* request. But in some situations (like an OAuth flow), you only -really need to look for authentication information once the user has reached -a certain URL (e.g. the redirect URL in OAuth). - -Fortunately, handling this situation is easy: just check to see what the -current URL is before creating the token in ``createToken()``:: - - // src/Security/ApiKeyAuthenticator.php - - // ... - use Symfony\Component\Security\Http\HttpUtils; - use Symfony\Component\HttpFoundation\Request; - - class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface - { - protected $httpUtils; - - public function __construct(HttpUtils $httpUtils) - { - $this->httpUtils = $httpUtils; - } - - public function createToken(Request $request, $providerKey) - { - // set the only URL where we should look for auth information - // and only return the token if we're at that URL - $targetUrl = '/login/check'; - if ($request->getPathInfo() !== $targetUrl) { - return; - } - - // ... - } - } - -That's it! Have fun! diff --git a/security/pre_authenticated.rst b/security/auth_providers.rst similarity index 59% rename from security/pre_authenticated.rst rename to security/auth_providers.rst index 4063ddeaba0..1478657ec1b 100644 --- a/security/pre_authenticated.rst +++ b/security/auth_providers.rst @@ -1,23 +1,88 @@ -.. index:: - single: Security; Pre authenticated providers +Built-in Authentication Providers +================================= -Using pre Authenticated Security Firewalls -========================================== +If you need to add authentication to your app, we recommend using +:doc:`Guard authentication ` because it gives you +full control over the process. -A lot of authentication modules are already provided by some web servers, -including Apache. These modules generally set some environment variables -that can be used to determine which user is accessing your application. Out of the -box, Symfony supports most authentication mechanisms. -These requests are called *pre authenticated* requests because the user is already -authenticated when reaching your application. +But, Symfony also offers a number of built-in authentication providers: systems +that are easier to implement, but harder to customize. If your authentication +use-case matches one of these exactly, they're a great option: -.. caution:: +.. toctree:: + :hidden: - :doc:`User impersonation ` is not - compatible with pre-authenticated firewalls. The reason is that - impersonation requires the authentication state to be maintained server-side, - but pre-authenticated information (``SSL_CLIENT_S_DN_Email``, ``REMOTE_USER`` - or other) is sent in each request. + form_login + json_login_setup + +* :doc:`form_login ` +* :ref:`http_basic ` +* :doc:`LDAP via HTTP Basic or Form Login ` +* :doc:`json_login ` +* :ref:`X.509 Client Certificate Authentication (x509) ` +* :ref:`REMOTE_USER Based Authentication (remote_user) ` +* ``simple_form`` +* ``simple_pre_auth`` + +.. _security-http_basic: + +HTTP Basic Authentication +------------------------- + +To support HTTP Basic authentication, add the ``http_basic`` key to your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + # ... + http_basic: + realm: Secured Area + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', array( + // ... + + 'firewalls' => array( + 'main' => array( + 'http_basic' => array( + 'realm' => 'Secured Area', + ), + ), + ), + )); + +That's it! Symfony will now be listening for any HTTP basic authentication data. +To load user information, it will use your configured :doc:`user provider `. + +.. _security-x509: X.509 Client Certificate Authentication --------------------------------------- @@ -37,8 +102,8 @@ Enable the x509 authentication for a particular firewall in the security configu # ... firewalls: - secured_area: - pattern: ^/ + main: + # ... x509: provider: your_user_provider @@ -55,7 +120,7 @@ Enable the x509 authentication for a particular firewall in the security configu - + @@ -68,8 +133,7 @@ Enable the x509 authentication for a particular firewall in the security configu // ... 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/', + 'main' => array( 'x509' => array( 'provider' => 'your_user_provider', ), @@ -94,8 +158,9 @@ in the x509 firewall configuration respectively. object of your choice. For more information on creating or configuring a user provider, see: - * :doc:`/security/custom_provider` - * :doc:`/security/entity_provider` + * :doc:`/security/user_provider` + +.. _security-remote_user: REMOTE_USER Based Authentication -------------------------------- @@ -114,8 +179,8 @@ corresponding firewall in your security configuration: # config/packages/security.yaml security: firewalls: - secured_area: - pattern: ^/ + main: + # ... remote_user: provider: your_user_provider @@ -127,7 +192,7 @@ corresponding firewall in your security configuration: xmlns:srv="http://symfony.com/schema/dic/services"> - + @@ -138,8 +203,7 @@ corresponding firewall in your security configuration: // config/packages/security.php $container->loadFromExtension('security', array( 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/', + 'main' => array( 'remote_user' => array( 'provider' => 'your_user_provider', ), @@ -156,4 +220,3 @@ key in the ``remote_user`` firewall configuration. Just like for X509 authentication, you will need to configure a "user provider". See :ref:`the previous note ` for more information. - diff --git a/security/csrf.rst b/security/csrf.rst index 9ae1370370d..80f1f6c9d6b 100644 --- a/security/csrf.rst +++ b/security/csrf.rst @@ -106,156 +106,8 @@ this can be customized on a form-by-form basis:: CSRF Protection in Login Forms ------------------------------ -`Login CSRF attacks`_ can be prevented using the same technique of adding hidden -CSRF tokens into the login forms. The Security component already provides CSRF -protection, but you need to configure some options before using it. - -.. tip:: - - If you're using a :doc:`Guard Authenticator `, - you'll need to validate the CSRF token manually inside of that class. See - :ref:`guard-csrf-protection` for details. - -First, configure the CSRF token provider used by the form login in your security -configuration. You can set this to use the default provider available in the -security component: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_token_generator: security.csrf.token_manager - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... - - 'firewalls' => array( - 'secured_area' => array( - // ... - 'form_login' => array( - // ... - 'csrf_token_generator' => 'security.csrf.token_manager', - ), - ), - ), - )); - -.. _csrf-login-template: - -Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF -token and store it as a hidden field of the form. By default, the HTML field -must be called ``_csrf_token`` and the string used to generate the value must -be ``authenticate``: - -.. code-block:: html+twig - - {# templates/security/login.html.twig #} - - {# ... #} -
- {# ... the login fields #} - - - - -
- -After this, you have protected your login form against CSRF attacks. - -.. tip:: - - You can change the name of the field by setting ``csrf_parameter`` and change - the token ID by setting ``csrf_token_id`` in your configuration: - - .. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - # ... - form_login: - # ... - csrf_parameter: _csrf_security_token - csrf_token_id: a_private_string - - .. code-block:: xml - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... - - 'firewalls' => array( - 'secured_area' => array( - // ... - 'form_login' => array( - // ... - 'csrf_parameter' => '_csrf_security_token', - 'csrf_token_id' => 'a_private_string', - ), - ), - ), - )); +See :doc:`/security/form_login_setup` for a login form that is protected from +CSRF attacks. CSRF Protection in HTML Forms ----------------------------- @@ -296,4 +148,3 @@ to check its validity:: } .. _`Cross-site request forgery`: http://en.wikipedia.org/wiki/Cross-site_request_forgery -.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests diff --git a/security/custom_authentication_provider.rst b/security/custom_authentication_provider.rst index 5088d4e7387..599b528b02d 100644 --- a/security/custom_authentication_provider.rst +++ b/security/custom_authentication_provider.rst @@ -4,17 +4,13 @@ How to Create a custom Authentication Provider ============================================== -.. tip:: +.. caution:: - Creating a custom authentication system is hard, and this article will walk - you through that process. But depending on your needs, you may be able - to solve your problem in a simpler manner, or via a community bundle: - - * :doc:`/security/guard_authentication` - * :doc:`/security/custom_password_authenticator` - * :doc:`/security/api_key_authentication` - * To authenticate via OAuth using a third-party service such as Google, Facebook - or Twitter, try using the `HWIOAuthBundle`_ community bundle. + Creating a custom authentication system is hard, and almost definitely + **not** needed. Instead, see :doc:`/security/guard_authentication` for a + simple way to create an authentication system you will love. Do **not** + keep reading unless you want to learn the lowest level details of + authentication. If you have read the article on :doc:`/security`, you understand the distinction Symfony makes between authentication and authorization in the @@ -287,13 +283,6 @@ the ``PasswordDigest`` header value matches with the user's password:: provider for the given token. In the case of multiple providers, the authentication manager will then move to the next provider in the list. -.. note:: - - While the :phpfunction:`hash_equals` function was introduced in PHP 5.6, - you are safe to use it with any PHP version in your Symfony application. In - PHP versions prior to 5.6, `Symfony Polyfill`_ (which is included in - Symfony) will define the function for you. - The Factory ----------- @@ -646,8 +635,7 @@ set to any desirable value per firewall. The rest is up to you! Any relevant configuration items can be defined in the factory and consumed or passed to the other classes in the container. -.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle + .. _`WSSE`: http://www.xml.com/pub/a/2003/12/17/dive.html .. _`nonce`: https://en.wikipedia.org/wiki/Cryptographic_nonce .. _`timing attacks`: https://en.wikipedia.org/wiki/Timing_attack -.. _`Symfony Polyfill`: https://github.com/symfony/polyfill diff --git a/security/custom_password_authenticator.rst b/security/custom_password_authenticator.rst deleted file mode 100644 index 6c413175e8a..00000000000 --- a/security/custom_password_authenticator.rst +++ /dev/null @@ -1,227 +0,0 @@ -.. index:: - single: Security; Custom Password Authenticator - -How to Create a Custom Form Password Authenticator -================================================== - -.. tip:: - - Check out :doc:`/security/guard_authentication` for a simpler and more - flexible way to accomplish custom authentication tasks like this. - -Imagine you want to allow access to your website only between 2pm and 4pm -UTC. In this article, you'll learn how to do this for a login form (i.e. where -your user submits their username and password). - -The Password Authenticator --------------------------- - -First, create a new class that implements -:class:`Symfony\\Component\\Security\\Http\\Authentication\\SimpleFormAuthenticatorInterface`. -Eventually, this will allow you to create custom logic for authenticating -the user:: - - // src/Security/TimeAuthenticator.php - namespace App\Security; - - use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; - use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - use Symfony\Component\Security\Core\Exception\BadCredentialsException; - use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface; - - class TimeAuthenticator implements SimpleFormAuthenticatorInterface - { - private $encoder; - - public function __construct(UserPasswordEncoderInterface $encoder) - { - $this->encoder = $encoder; - } - - public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) - { - try { - $user = $userProvider->loadUserByUsername($token->getUsername()); - } catch (UsernameNotFoundException $exception) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException('Invalid username or password'); - } - - $currentUser = $token->getUser(); - - if ($currentUser instanceof UserInterface) { - if ($currentUser->getPassword() !== $user->getPassword()) { - throw new BadCredentialsException('The credentials were changed from another session.'); - } - } else { - if ('' === ($givenPassword = $token->getCredentials())) { - throw new BadCredentialsException('The given password cannot be empty.'); - } - if (!$this->encoder->isPasswordValid($user, $givenPassword)) { - throw new BadCredentialsException('The given password is invalid.'); - } - } - - $currentHour = date('G'); - if ($currentHour < 14 || $currentHour > 16) { - // CAUTION: this message will be returned to the client - // (so don't put any un-trusted messages / error strings here) - throw new CustomUserMessageAuthenticationException( - 'You can only log in between 2 and 4!', - array(), // Message Data - 412 // HTTP 412 Precondition Failed - ); - } - - return new UsernamePasswordToken( - $user, - $user->getPassword(), - $providerKey, - $user->getRoles() - ); - } - - public function supportsToken(TokenInterface $token, $providerKey) - { - return $token instanceof UsernamePasswordToken - && $token->getProviderKey() === $providerKey; - } - - public function createToken(Request $request, $username, $password, $providerKey) - { - return new UsernamePasswordToken($username, $password, $providerKey); - } - } - -How it Works ------------- - -Great! Now you just need to setup some :ref:`security-password-authenticator-config`. -But first, you can find out more about what each method in this class does. - -1) createToken -~~~~~~~~~~~~~~ - -When Symfony begins handling a request, ``createToken()`` is called, where -you create a :class:`Symfony\\Component\\Security\\Core\\Authentication\\Token\\TokenInterface` -object that contains whatever information you need in ``authenticateToken()`` -to authenticate the user (e.g. the username and password). - -Whatever token object you create here will be passed to you later in ``authenticateToken()``. - -2) supportsToken -~~~~~~~~~~~~~~~~ - -.. include:: _supportsToken.rst.inc - -3) authenticateToken -~~~~~~~~~~~~~~~~~~~~ - -If ``supportsToken()`` returns ``true``, Symfony will now call ``authenticateToken()``. -Your job here is to check that the token is allowed to log in by first -getting the ``User`` object via the user provider and then, by checking the password -and the current time. - -.. note:: - - The "flow" of how you get the ``User`` object and determine whether or not - the token is valid (e.g. checking the password), may vary based on your - requirements. - -Ultimately, your job is to return a *new* token object that is "authenticated" -(i.e. it has at least 1 role set on it) and which has the ``User`` object -inside of it. - -Inside this method, the password encoder is needed to check the password's validity:: - - $isPasswordValid = $this->encoder->isPasswordValid($user, $token->getCredentials()); - -This is a service that is already available in Symfony and it uses the password algorithm -that is configured in the security configuration (e.g. ``security.yaml``) under -the ``encoders`` key. Below, you'll see how to inject that into the ``TimeAuthenticator``. - -.. _security-password-authenticator-config: - -Configuration -------------- - -Now, make sure your ``TimeAuthenticator`` is registered as as service. If you're -using the :ref:`default services.yaml configuration `, -that happens automatically. - -Finally, activate the service in the ``firewalls`` section of the security configuration -using the ``simple_form`` key: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - firewalls: - secured_area: - pattern: ^/admin - # ... - simple_form: - authenticator: App\Security\TimeAuthenticator - check_path: login_check - login_path: login - - .. code-block:: xml - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - - // ... - use App\Security\TimeAuthenticator; - - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - 'pattern' => '^/admin', - 'simple_form' => array( - 'provider' => ..., - 'authenticator' => App\Security\TimeAuthenticator::class, - 'check_path' => 'login_check', - 'login_path' => 'login', - ), - ), - ), - )); - -The ``simple_form`` key has the same options as the normal ``form_login`` -option, but with the additional ``authenticator`` key that points to the -new service. For details, see :ref:`reference-security-firewall-form-login`. - -If creating a login form in general is new to you or you don't understand -the ``check_path`` or ``login_path`` options, see :doc:`/security/form_login`. diff --git a/security/custom_provider.rst b/security/custom_provider.rst deleted file mode 100644 index a0b86c6240f..00000000000 --- a/security/custom_provider.rst +++ /dev/null @@ -1,355 +0,0 @@ -.. index:: - single: Security; User Provider - -How to Create a custom User Provider -==================================== - -Part of Symfony's standard authentication process depends on "user providers". -When a user submits a username and password, the authentication layer asks -the configured user provider to return a user object for a given username. -Symfony then checks whether the password of this user is correct and generates -a security token so the user stays authenticated during the current session. -Out of the box, Symfony has four user providers: ``memory``, ``entity``, -``ldap`` and ``chain``. In this article you'll see how you can create your -own user provider, which could be useful if your users are accessed via a -custom database, a file, or - as shown in this example - a web service. - -Create a User Class -------------------- - -First, regardless of *where* your user data is coming from, you'll need to -create a ``User`` class that represents that data. The ``User`` can look -however you want and contain any data. The only requirement is that the -class implements :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. -The methods in this interface should therefore be defined in the custom user -class: :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getRoles`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getPassword`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getSalt`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUsername`, -:method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::eraseCredentials`. -It may also be useful to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface` interface, -which defines a method to check if the user is equal to the current user. This -interface requires an :method:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface::isEqualTo` -method. - -This is how your ``WebserviceUser`` class looks in action:: - - // src/Security/User/WebserviceUser.php - namespace App\Security\User; - - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\User\EquatableInterface; - - class WebserviceUser implements UserInterface, EquatableInterface - { - private $username; - private $password; - private $salt; - private $roles; - - public function __construct($username, $password, $salt, array $roles) - { - $this->username = $username; - $this->password = $password; - $this->salt = $salt; - $this->roles = $roles; - } - - public function getRoles() - { - return $this->roles; - } - - public function getPassword() - { - return $this->password; - } - - public function getSalt() - { - return $this->salt; - } - - public function getUsername() - { - return $this->username; - } - - public function eraseCredentials() - { - } - - public function isEqualTo(UserInterface $user) - { - if (!$user instanceof WebserviceUser) { - return false; - } - - if ($this->password !== $user->getPassword()) { - return false; - } - - if ($this->salt !== $user->getSalt()) { - return false; - } - - if ($this->username !== $user->getUsername()) { - return false; - } - - return true; - } - } - -If you have more information about your users - like a "first name" - then -you can add a ``firstName`` field to hold that data. - -Create a User Provider ----------------------- - -Now that you have a ``User`` class, you'll create a user provider, which will -grab user information from some web service, create a ``WebserviceUser`` object, -and populate it with data. - -The user provider is just a plain PHP class that has to implement the -:class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`, -which requires three methods to be defined: ``loadUserByUsername($username)``, -``refreshUser(UserInterface $user)``, and ``supportsClass($class)``. For -more details, see :class:`Symfony\\Component\\Security\\Core\\User\\UserProviderInterface`. - -Here's an example of how this might look:: - - // src/Security/User/WebserviceUserProvider.php - namespace App\Security\User; - - use App\Security\User\WebserviceUser; - use Symfony\Component\Security\Core\User\UserProviderInterface; - use Symfony\Component\Security\Core\User\UserInterface; - use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; - use Symfony\Component\Security\Core\Exception\UnsupportedUserException; - - class WebserviceUserProvider implements UserProviderInterface - { - public function loadUserByUsername($username) - { - return $this->fetchUser($username); - } - - public function refreshUser(UserInterface $user) - { - if (!$user instanceof WebserviceUser) { - throw new UnsupportedUserException( - sprintf('Instances of "%s" are not supported.', get_class($user)) - ); - } - - $username = $user->getUsername(); - - return $this->fetchUser($username); - } - - public function supportsClass($class) - { - return WebserviceUser::class === $class; - } - - private function fetchUser($username) - { - // make a call to your webservice here - $userData = ... - // pretend it returns an array on success, false if there is no user - - if ($userData) { - $password = '...'; - - // ... - - return new WebserviceUser($username, $password, $salt, $roles); - } - - throw new UsernameNotFoundException( - sprintf('Username "%s" does not exist.', $username) - ); - } - } - -Create a Service for the User Provider --------------------------------------- - -Now you make the user provider available as a service. If you're using the -:ref:`default services.yaml configuration `, -this happens automatically. - -Modify ``security.yaml`` ------------------------- - -Everything comes together in your security configuration. Add the user provider -to the list of providers in the "security" config. Choose a name for the user provider -(e.g. "webservice") and mention the ``id`` of the service you just defined. - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - providers: - webservice: - id: App\Security\User\WebserviceUserProvider - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - use App\Security\User\WebserviceUserProvider; - - $container->loadFromExtension('security', array( - // ... - - 'providers' => array( - 'webservice' => array( - 'id' => WebserviceUserProvider::class, - ), - ), - )); - -Symfony also needs to know how to encode passwords that are supplied by website -users, e.g. by filling in a login form. You can do this by adding a line to the -"encoders" section in your security configuration: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - encoders: - App\Security\User\WebserviceUser: bcrypt - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - use App\Security\User\WebserviceUser; - - $container->loadFromExtension('security', array( - // ... - - 'encoders' => array( - WebserviceUser::class => 'bcrypt', - ), - // ... - )); - -The value here should correspond with however the passwords were originally -encoded when creating your users (however those users were created). When -a user submits their password, it's encoded using this algorithm and the result -is compared to the hashed password returned by your ``getPassword()`` method. - -.. sidebar:: Specifics on how Passwords are Encoded - - Symfony uses a specific method to combine the salt and encode the password - before comparing it to your encoded password. If ``getSalt()`` returns - nothing, then the submitted password is simply encoded using the algorithm - you specify in ``security.yaml``. If a salt *is* specified, then the following - value is created and *then* hashed via the algorithm:: - - $password.'{'.$salt.'}' - - If your external users have their passwords salted via a different method, - then you'll need to do a bit more work so that Symfony properly encodes - the password. That is beyond the scope of this article, but would include - sub-classing ``MessageDigestPasswordEncoder`` and overriding the - ``mergePasswordAndSalt()`` method. - - Additionally, you can configure the details of the algorithm used to hash - passwords. In this example, the application sets explicitly the cost of - the bcrypt hashing: - - .. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - encoders: - App\Security\User\WebserviceUser: - algorithm: bcrypt - cost: 12 - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - use App\Security\User\WebserviceUser; - - $container->loadFromExtension('security', array( - // ... - - 'encoders' => array( - WebserviceUser::class => array( - 'algorithm' => 'bcrypt', - 'cost' => 12, - ), - ), - )); - -.. _MessageDigestPasswordEncoder: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Security/Core/Encoder/MessageDigestPasswordEncoder.php diff --git a/security/entity_provider.rst b/security/entity_provider.rst index 38114aa3adb..376eddeda95 100644 --- a/security/entity_provider.rst +++ b/security/entity_provider.rst @@ -5,226 +5,25 @@ How to Load Security Users from the Database (the Entity Provider) ================================================================== -Symfony's security system can load security users from anywhere - like a -database, via Active Directory or an OAuth server. This article will show -you how to load your users from the database via a Doctrine entity. - -Introduction ------------- - -Loading users via a Doctrine entity has 2 basic steps: - -#. :ref:`Create your User entity ` -#. :ref:`Configure security.yaml to load from your entity ` - -Afterwards, you can learn more about :ref:`forbidding inactive users `, -:ref:`using a custom query ` -and :ref:`user serialization to the session ` - -.. _security-crete-user-entity: -.. _the-data-model: - -1) Create your User Entity --------------------------- - -Before you begin, run this command to add support for the Symfony security: - -.. code-block:: terminal - - $ composer require symfony/security-bundle - -For this article, suppose that you already have a ``User`` entity -with the following fields: ``id``, ``username``, ``password``, -``email`` and ``isActive``:: - - // src/Entity/User.php - namespace App\Entity; - - use Doctrine\ORM\Mapping as ORM; - use Symfony\Component\Security\Core\User\UserInterface; - - /** - * @ORM\Table(name="app_users") - * @ORM\Entity(repositoryClass="App\Repository\UserRepository") - */ - class User implements UserInterface, \Serializable - { - /** - * @ORM\Column(type="integer") - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - */ - private $id; - - /** - * @ORM\Column(type="string", length=25, unique=true) - */ - private $username; - - /** - * @ORM\Column(type="string", length=64) - */ - private $password; - - /** - * @ORM\Column(type="string", length=254, unique=true) - */ - private $email; - - /** - * @ORM\Column(name="is_active", type="boolean") - */ - private $isActive; - - public function __construct() - { - $this->isActive = true; - // may not be needed, see section on salt below - // $this->salt = md5(uniqid('', true)); - } - - public function getUsername() - { - return $this->username; - } - - public function getSalt() - { - // you *may* need a real salt depending on your encoder - // see section on salt below - return null; - } - - public function getPassword() - { - return $this->password; - } - - public function getRoles() - { - return array('ROLE_USER'); - } - - public function eraseCredentials() - { - } - - /** @see \Serializable::serialize() */ - public function serialize() - { - return serialize(array( - $this->id, - $this->username, - $this->password, - // see section on salt below - // $this->salt, - )); - } - - /** @see \Serializable::unserialize() */ - public function unserialize($serialized) - { - list ( - $this->id, - $this->username, - $this->password, - // see section on salt below - // $this->salt - ) = unserialize($serialized, array('allowed_classes' => false)); - } - } - -To make things shorter, some of the getter and setter methods aren't shown. -But you can generate these manually or with your own IDE. - -.. caution:: - - In the example above, the User entity's table name is "app_users" because - "USER" is a SQL reserved word. If you wish to call your table name "user", - `it must be quoted with backticks`_ to avoid errors. The annotation should - look like ``@ORM\Table(name="`user`")``. - -Next, make sure to :ref:`create the database table `: - -.. code-block:: terminal - - $ php bin/console doctrine:migrations:diff - $ php bin/console doctrine:migrations:migrate - -What's this UserInterface? -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -So far, this is just a normal entity. But to use this class in the -security system, it must implement -:class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. This -forces the class to have the five following methods: - -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getRoles` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getPassword` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getSalt` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::getUsername` -* :method:`Symfony\\Component\\Security\\Core\\User\\UserInterface::eraseCredentials` - -To learn more about each of these, see :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`. - -.. caution:: - - The ``eraseCredentials()`` method is only meant to clean up possibly stored - plain text passwords (or similar credentials). Be careful what to erase - if your user class is also mapped to a database as the modified object - will likely be persisted during the request. - -What do the serialize and unserialize Methods do? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -At the end of each request, the User object is serialized to the session. -On the next request, it's unserialized. To help PHP do this correctly, you -need to implement ``Serializable``. But you don't need to serialize everything: -you only need a few fields (the ones shown above plus a few extra if you added -other important fields to your user entity). On each request, the ``id`` is used -to query for a fresh ``User`` object from the database. - -Want to know more? See :ref:`security-serialize-equatable`. - -.. _authenticating-someone-against-a-database: -.. _security-config-entity-provider: - -2) Configure Security to load from your Entity ----------------------------------------------- - -Now that you have a ``User`` entity that implements ``UserInterface``, you -just need to tell Symfony's security system about it in ``security.yaml``. - -In this example, the user will enter their username and password via HTTP -basic authentication. Symfony will query for a ``User`` entity matching -the username and then check the password (more on passwords in a moment): +Each User class in your app will usually need its own :doc:`user provider `. +If you're loading users from the database, you can use the built-in ``entity`` provider: .. configuration-block:: .. code-block:: yaml # config/packages/security.yaml - security: - encoders: - App\Entity\User: - algorithm: bcrypt - # ... providers: our_db_provider: entity: class: App\Entity\User + # the property to query by - e.g. username, email, etc property: username # if you're using multiple entity managers # manager_name: customer - firewalls: - main: - pattern: ^/ - http_basic: ~ - provider: our_db_provider - # ... .. code-block:: xml @@ -238,20 +37,12 @@ the username and then check the password (more on passwords in a moment): http://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - - - - @@ -262,14 +53,6 @@ the username and then check the password (more on passwords in a moment): use App\Entity\User; $container->loadFromExtension('security', array( - 'encoders' => array( - User::class => array( - 'algorithm' => 'bcrypt', - ), - ), - - // ... - 'providers' => array( 'our_db_provider' => array( 'entity' => array( @@ -278,162 +61,25 @@ the username and then check the password (more on passwords in a moment): ), ), ), - 'firewalls' => array( - 'main' => array( - 'pattern' => '^/', - 'http_basic' => null, - 'provider' => 'our_db_provider', - ), - ), // ... )); -First, the ``encoders`` section tells Symfony to expect that the passwords -in the database will be encoded using ``bcrypt``. Second, the ``providers`` -section creates a "user provider" called ``our_db_provider`` that knows to -query from your ``App\Entity\User`` entity by the ``username`` property. The -name ``our_db_provider`` isn't important: it just needs to match the value -of the ``provider`` key under your firewall. Or, if you don't set the ``provider`` -key under your firewall, the first "user provider" is automatically used. - -Creating your First User -~~~~~~~~~~~~~~~~~~~~~~~~ - -To add users, you can implement a :doc:`registration form ` -or add some `fixtures`_. This is just a normal entity, so there's nothing -tricky, *except* that you need to encode each user's password. But don't -worry, Symfony gives you a service that will do this for you. See :doc:`/security/password_encoding` -for details. - -Below is an export of the ``app_users`` table from MySQL with user ``admin`` -and password ``admin`` (which has been encoded). - -.. code-block:: terminal - - $ mysql> SELECT * FROM app_users; - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - | id | username | password | email | is_active | - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - | 1 | admin | $2a$08$jHZj/wJfcVKlIwr5AvR78euJxYK7Ku5kURNhNx.7.CSIJ3Pq6LEPC | admin@example.com | 1 | - +----+----------+--------------------------------------------------------------+--------------------+-----------+ - -.. sidebar:: Do you need to use a Salt property? - - If you use ``bcrypt`` or ``argon2i``, no. Otherwise, yes. All passwords must - be hashed with a salt, but ``bcrypt`` and ``argon2i`` do this internally. - Since this tutorial *does* use ``bcrypt``, the ``getSalt()`` method in - ``User`` can just return ``null`` (it's not used). If you use a different - algorithm, you'll need to uncomment the ``salt`` lines in the ``User`` - entity and add a persisted ``salt`` property. - -.. _security-advanced-user-interface: - -Forbid Inactive Users (AdvancedUserInterface) ---------------------------------------------- - -.. versionadded:: 4.1 - The ``AdvancedUserInterface`` class was deprecated in Symfony 4.1 and no - alternative is provided. If you need this functionality in your application, - implement :doc:`a custom user checker ` that - performs the needed checks. - -If a User's ``isActive`` property is set to ``false`` (i.e. ``is_active`` -is 0 in the database), the user will still be able to login to the site -normally. This is easily fixable. - -To exclude inactive users, change your ``User`` class to implement -:class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface`. -This extends :class:`Symfony\\Component\\Security\\Core\\User\\UserInterface`, -so you only need the new interface:: - - // src/Entity/User.php - - use Symfony\Component\Security\Core\User\AdvancedUserInterface; - // ... - - class User implements AdvancedUserInterface, \Serializable - { - // ... - - public function isAccountNonExpired() - { - return true; - } - - public function isAccountNonLocked() - { - return true; - } - - public function isCredentialsNonExpired() - { - return true; - } - - public function isEnabled() - { - return $this->isActive; - } - - // serialize and unserialize must be updated - see below - public function serialize() - { - return serialize(array( - // ... - $this->isActive, - )); - } - public function unserialize($serialized) - { - list ( - // ... - $this->isActive, - ) = unserialize($serialized); - } - } - -The :class:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface` -interface adds four extra methods to validate the account status: - -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isAccountNonExpired` - checks whether the user's account has expired; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isAccountNonLocked` - checks whether the user is locked; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isCredentialsNonExpired` - checks whether the user's credentials (password) has expired; -* :method:`Symfony\\Component\\Security\\Core\\User\\AdvancedUserInterface::isEnabled` - checks whether the user is enabled. - -If *any* of these return ``false``, the user won't be allowed to login. You -can choose to have persisted properties for all of these, or whatever you -need (in this example, only ``isActive`` pulls from the database). - -So what's the difference between the methods? Each returns a slightly different -error message (and these can be translated when you render them in your login -template to customize them further). - -.. note:: - - If you use ``AdvancedUserInterface``, you also need to add any of the - properties used by these methods (like ``isActive``) to the ``serialize()`` - and ``unserialize()`` methods. If you *don't* do this, your user may - not be deserialized correctly from the session on each request. - -Congrats! Your database-loading security system is all setup! Next, add a -true :doc:`login form ` instead of HTTP Basic -or keep reading for other topics. +The ``providers`` section creates a "user provider" called ``our_db_provider`` that +knows to query from your ``App\Entity\User`` entity by the ``username`` property. +The name ``our_db_provider`` isn't important: it's not used, unless you have multiple +user providers and need to specify which user provider to use via the ``provider`` +key under your firewall. .. _authenticating-someone-with-a-custom-entity-provider: Using a Custom Query to Load the User ------------------------------------- -It would be great if a user could login with their username *or* email, as -both are unique in the database. Unfortunately, the native entity provider -is only able to handle querying via a single property on the user. - -To do this, make your ``UserRepository`` implement a special +The ``entity`` provider can only query from one *specific* field, specified by the +``property`` config key. If you want a bit more control over this - e.g. you want +to find a user by ``email`` *or* ``username``, you can do that by making your +``UserRepository`` implement a special :class:`Symfony\\Bridge\\Doctrine\\Security\\User\\UserLoaderInterface`. This interface only requires one method: ``loadUserByUsername($username)``:: @@ -456,7 +102,7 @@ interface only requires one method: ``loadUserByUsername($username)``:: } } -To finish this, just remove the ``property`` key from the user provider in +To finish this, remove the ``property`` key from the user provider in ``security.yaml``: .. configuration-block:: @@ -508,47 +154,6 @@ To finish this, just remove the ``property`` key from the user provider in ), )); -This tells Symfony to *not* query automatically for the User. Instead, when -someone logs in, the ``loadUserByUsername()`` method on ``UserRepository`` -will be called. - -.. _security-serialize-equatable: - -Understanding serialize and how a User is Saved in the Session --------------------------------------------------------------- - -If you're curious about the importance of the ``serialize()`` method inside -the ``User`` class or how the User object is serialized or deserialized, then -this section is for you. If not, feel free to skip this. - -Once the user is logged in, the entire User object is serialized into the -session. On the next request, the User object is deserialized. Then, the value -of the ``id`` property is used to re-query for a fresh User object from the -database. Finally, the fresh User object is compared to the deserialized -User object to make sure that they represent the same user. For example, if -the ``username`` on the 2 User objects doesn't match for some reason, then -the user will be logged out for security reasons. - -Even though this all happens automatically, there are a few important side-effects. - -First, the :phpclass:`Serializable` interface and its ``serialize()`` and ``unserialize()`` -methods have been added to allow the ``User`` class to be serialized -to the session. This may or may not be needed depending on your setup, -but it's probably a good idea. In theory, only the ``id`` needs to be serialized, -because the :method:`Symfony\\Bridge\\Doctrine\\Security\\User\\EntityUserProvider::refreshUser` -method refreshes the user on each request by using the ``id`` (as explained -above). This gives us a "fresh" User object. - -But Symfony also uses the ``username``, ``salt``, and ``password`` to verify -that the User has not changed between requests (it also calls your ``AdvancedUserInterface`` -methods if you implement it). Failing to serialize these may cause you to -be logged out on each request. If your user implements the -:class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`, -then instead of these properties being checked, your :method:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface::isEqualTo` method -is called, and you can check whatever properties you want. Unless -you understand this, you probably *won't* need to implement this interface -or worry about it. - -.. _fixtures: https://symfony.com/doc/master/bundles/DoctrineFixturesBundle/index.html -.. _FOSUserBundle: https://github.com/FriendsOfSymfony/FOSUserBundle -.. _`it must be quoted with backticks`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words +This tells Symfony to *not* query automatically for the User. Instead, when needed +(e.g. because ``switch_user``, ``remember_me`` or some other security feature is +activated), the ``loadUserByUsername()`` method on ``UserRepository`` will be called. diff --git a/security/expressions.rst b/security/expressions.rst index 657ffc1dca4..d445bc6218b 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -92,6 +92,34 @@ Additionally, you have access to a number of functions inside the expression: true if the user has actually logged in during this session (i.e. is full-fledged). +TODO - moved from main security chapter +--- + +.. _security-template-expression: + +You can also use expressions inside your templates: + +.. configuration-block:: + + .. code-block:: html+jinja + + {% if is_granted(expression( + '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' + )) %} + Delete + {% endif %} + + .. code-block:: html+php + + isGranted(new Expression( + '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' + ))): ?> + Delete + + +For more details on expressions and security, see :doc:`/security/expressions`. + + Learn more ---------- diff --git a/security/firewall_restriction.rst b/security/firewall_restriction.rst index 82dc7c107ff..d029f5bfd7e 100644 --- a/security/firewall_restriction.rst +++ b/security/firewall_restriction.rst @@ -1,8 +1,8 @@ .. index:: single: Security; Restrict Security Firewalls to a Request -How to Restrict Firewalls to a Specific Request -=============================================== +How to Restrict Firewalls by Pattern, Host or HTTP Method +========================================================= When using the Security component, you can create firewalls that match certain request options. In most cases, matching against the URL is sufficient, but in special cases you can further diff --git a/security/force_https.rst b/security/force_https.rst index 3bcc6890dbd..50ab5b3ed8b 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -4,10 +4,15 @@ How to Force HTTPS or HTTP for different URLs ============================================= +.. tip:: + + The *best* policy is to force ``https`` on all URLs, which can be done via + your web server configuration or ``access_control``. + You can force areas of your site to use the HTTPS protocol in the security config. This is done through the ``access_control`` rules using the ``requires_channel`` -option. For example, if you want to force all URLs starting with ``/secure`` -to use HTTPS then you could use the following configuration: +option. For example, suppose you want to force all URLs starting with ``/secure`` +to use HTTPS as well as the login form: .. configuration-block:: @@ -19,6 +24,7 @@ to use HTTPS then you could use the following configuration: access_control: - { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https } + - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } .. code-block:: xml @@ -34,6 +40,10 @@ to use HTTPS then you could use the following configuration: + @@ -49,59 +59,16 @@ to use HTTPS then you could use the following configuration: 'role' => 'ROLE_ADMIN', 'requires_channel' => 'https', ), + array( + 'path' => '^/login', + 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'requires_channel' => 'https', + ), ), )); -The login form itself needs to allow anonymous access, otherwise users will -be unable to authenticate. To force it to use HTTPS you can still use -``access_control`` rules by using the ``IS_AUTHENTICATED_ANONYMOUSLY`` -role: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... - - access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - - .. code-block:: xml - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - // ... - - 'access_control' => array( - array( - 'path' => '^/login', - 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', - 'requires_channel' => 'https', - ), - ), - )); +See :doc:`/security/access_control` for more details about ``access_control`` +in general. It is also possible to specify using HTTPS in the routing configuration, see :doc:`/routing/scheme` for more details. diff --git a/security/form_login.rst b/security/form_login.rst index 2984483c712..07b271bb871 100644 --- a/security/form_login.rst +++ b/security/form_login.rst @@ -1,15 +1,399 @@ .. index:: single: Security; Customizing form login redirect -How to Customize Redirect After Form Login -========================================== +Using the form_login Authentication Provider +============================================ -Using a :doc:`form login ` for authentication is a -common, and flexible, method for handling authentication in Symfony. This -article explains how to customize the URL which the user is redirected to after -a successful or failed login. Check out the full -:doc:`form login configuration reference ` to -learn of the possible customization options. +.. caution:: + + To have complete control over your login form, we recommend building a + :doc:`form login authentication with Guard `. + +Symfony comes with a built-in ``form_login`` system that handles a login form +POST automatically. Before you start, make sure you've followed the +:doc:`Security Guide ` to create your User class. + +form_login Setup +---------------- + +First, enable ``form_login`` under your firewall: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + main: + anonymous: ~ + form_login: + login_path: login + check_path: login + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'main' => array( + 'anonymous' => null, + 'form_login' => array( + 'login_path' => 'login', + 'check_path' => 'login', + ), + ), + ), + )); + +.. tip:: + + The ``login_path`` and ``check_path`` can also be route names (but cannot + have mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` has no + default value). + +Now, when the security system initiates the authentication process, it will +redirect the user to the login form ``/login``. Implementing this login form +is your job. First, create a new ``SecurityController`` inside a bundle:: + + // src/Controller/SecurityController.php + namespace App\Controller; + + use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + + class SecurityController extends AbstractController + { + } + +Next, configure the route that you earlier used under your ``form_login`` +configuration (``login``): + +.. configuration-block:: + + .. code-block:: php-annotations + + // src/Controller/SecurityController.php + + // ... + use Symfony\Component\Routing\Annotation\Route; + + class SecurityController extends AbstractController + { + /** + * @Route("/login", name="login") + */ + public function login() + { + } + } + + .. code-block:: yaml + + # config/routes.yaml + login: + path: /login + controller: App\Controller\SecurityController::login + + .. code-block:: xml + + + + + + + App\Controller\SecurityController::login + + + + .. code-block:: php + + // config/routes.php + use App\Controller\SecurityController; + use Symfony\Component\Routing\RouteCollection; + use Symfony\Component\Routing\Route; + + $routes = new RouteCollection(); + $routes->add('login', new Route('/login', array( + '_controller' => array(SecurityController::class, 'login'), + ))); + + return $routes; + +Great! Next, add the logic to ``login()`` that displays the login form:: + + // src/Controller/SecurityController.php + use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; + + public function login(AuthenticationUtils $authenticationUtils) + { + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', array( + 'last_username' => $lastUsername, + 'error' => $error, + )); + } + +.. note:: + + If you get an error that the ``$authenticationUtils`` argument is missing, + it's probably because the controllers of your application are not defined as + services and tagged with the ``controller.service_arguments`` tag, as done + in the :ref:`default services.yaml configuration `. + +Don't let this controller confuse you. As you'll see in a moment, when the +user submits the form, the security system automatically handles the form +submission for you. If the user submits an invalid username or password, +this controller reads the form submission error from the security system, +so that it can be displayed back to the user. + +In other words, your job is to *display* the login form and any login errors +that may have occurred, but the security system itself takes care of checking +the submitted username and password and authenticating the user. + +Finally, create the template: + +.. code-block:: html+twig + + {# templates/security/login.html.twig #} + {# ... you will probably extend your base template, like base.html.twig #} + + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +
+ + + + + + + {# + If you want to control the URL the user + is redirected to on success (more details below) + + #} + + +
+ +.. tip:: + + The ``error`` variable passed into the template is an instance of + :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. + It may contain more information - or even sensitive information - about + the authentication failure, so use it wisely! + +The form can look like anything, but it usually follows some conventions: + +* The ``
`` element sends a ``POST`` request to the ``login`` route, since + that's what you configured under the ``form_login`` key in ``security.yaml``; +* The username field has the name ``_username`` and the password field has the + name ``_password``. + +.. tip:: + + Actually, all of this can be configured under the ``form_login`` key. See + :ref:`reference-security-firewall-form-login` for more details. + +.. caution:: + + This login form is currently not protected against CSRF attacks. Read + :ref:`form_login-csrf` on how to protect your login form. + +And that's it! When you submit the form, the security system will automatically +check the user's credentials and either authenticate the user or send the +user back to the login form where the error can be displayed. + +To review the whole process: + +#. The user tries to access a resource that is protected; +#. The firewall initiates the authentication process by redirecting the + user to the login form (``/login``); +#. The ``/login`` page renders login form via the route and controller created + in this example; +#. The user submits the login form to ``/login``; +#. The security system intercepts the request, checks the user's submitted + credentials, authenticates the user if they are correct, and sends the + user back to the login form if they are not. + +.. _form_login-csrf: + +CSRF Protection in Login Forms +------------------------------ + +`Login CSRF attacks`_ can be prevented using the same technique of adding hidden +CSRF tokens into the login forms. The Security component already provides CSRF +protection, but you need to configure some options before using it. + +First, configure the CSRF token provider used by the form login in your security +configuration. You can set this to use the default provider available in the +security component: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + secured_area: + # ... + form_login: + # ... + csrf_token_generator: security.csrf.token_manager + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', array( + // ... + + 'firewalls' => array( + 'secured_area' => array( + // ... + 'form_login' => array( + // ... + 'csrf_token_generator' => 'security.csrf.token_manager', + ), + ), + ), + )); + +.. _csrf-login-template: + +Then, use the ``csrf_token()`` function in the Twig template to generate a CSRF +token and store it as a hidden field of the form. By default, the HTML field +must be called ``_csrf_token`` and the string used to generate the value must +be ``authenticate``: + +.. code-block:: html+twig + + {# templates/security/login.html.twig #} + + {# ... #} + + {# ... the login fields #} + + + + +
+ +After this, you have protected your login form against CSRF attacks. + +.. tip:: + + You can change the name of the field by setting ``csrf_parameter`` and change + the token ID by setting ``csrf_token_id`` in your configuration: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + # ... + + firewalls: + secured_area: + # ... + form_login: + # ... + csrf_parameter: _csrf_security_token + csrf_token_id: a_private_string + + .. code-block:: xml + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', array( + // ... + + 'firewalls' => array( + 'secured_area' => array( + // ... + 'form_login' => array( + // ... + 'csrf_parameter' => '_csrf_security_token', + 'csrf_token_id' => 'a_private_string', + ), + ), + ), + )); Redirecting after Success ------------------------- @@ -387,20 +771,4 @@ are now fully customized: -Redirecting to the Last Accessed Page with ``TargetPathTrait`` --------------------------------------------------------------- - -The last request URI is stored in a session variable named -``_security..target_path`` (e.g. ``_security.main.target_path`` -if the name of your firewall is ``main``). Most of the times you don't have to -deal with this low level session variable. However, if you ever need to get or -remove this variable, it's better to use the -:class:`Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait` utility:: - - // ... - use Symfony\Component\Security\Http\Util\TargetPathTrait; - - $targetPath = $this->getTargetPath($request->getSession(), $providerKey); - - // equivalent to: - // $targetPath = $request->getSession()->get('_security.'.$providerKey.'.target_path'); +.. _`Login CSRF attacks`: https://en.wikipedia.org/wiki/Cross-site_request_forgery#Forging_login_requests diff --git a/security/form_login_setup.rst b/security/form_login_setup.rst index adcd3582872..8e807a6063f 100644 --- a/security/form_login_setup.rst +++ b/security/form_login_setup.rst @@ -1,382 +1,318 @@ -How to Build a Traditional Login Form -===================================== +How to Build a Login Form +========================= -.. tip:: +.. seealso:: - If you need a login form and are storing users in some sort of a database, - then you should consider using `FOSUserBundle`_, which helps you build - your ``User`` object and gives you many routes and controllers for common - tasks like login, registration and forgot password. + If you're looking for the ``form_login`` firewall option, see + :doc:`/security/form_login`. -In this entry, you'll build a traditional login form. Of course, when the -user logs in, you can load your users from anywhere - like the database. -See :ref:`security-user-providers` for details. +Ready to create a login form? First, make sure you've followed the main +:doc:`Security Guide ` to install security and create your ``User`` +class. -First, enable form login under your firewall: +Generating the Login Form +------------------------- -.. configuration-block:: +Creating a pwoerful login form is easy thanks to the ``make:auth`` command from +`MakerBundle`_. Depending on your setup, you may be asked different questions +and your generated code may be slightly different: - .. code-block:: yaml +.. code-block:: terminal - # config/packages/security.yaml - security: - # ... + $ php bin/console make:auth - firewalls: - main: - anonymous: ~ - form_login: - login_path: login - check_path: login + What style of authentication do you want? [Empty authenticator]: + [0] Empty authenticator + [1] Login form authenticator + > 1 - .. code-block:: xml + The class name of the authenticator to create (e.g. AppCustomAuthenticator): + > LoginFormAuthenticator - - - - - - - - - - - + Choose a name for the controller class (e.g. SecurityController) [SecurityController]: + > SecurityController - .. code-block:: php + created: src/Security/LoginFormAuthenticator.php + updated: config/packages/security.yaml + created: src/Controller/SecurityController.php + created: templates/security/login.html.twig - // config/packages/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'main' => array( - 'anonymous' => null, - 'form_login' => array( - 'login_path' => 'login', - 'check_path' => 'login', - ), - ), - ), - )); - -.. tip:: - - The ``login_path`` and ``check_path`` can also be route names (but cannot - have mandatory wildcards - e.g. ``/login/{foo}`` where ``foo`` has no - default value). - -Now, when the security system initiates the authentication process, it will -redirect the user to the login form ``/login``. Implementing this login form -is your job. First, create a new ``SecurityController`` inside a bundle:: +.. versionadded:: 1.8 + Support for login form authentication was added to ``make:auth`` in MakerBundle 1.8. + +This generates three things: (1) a login route & controller, (2) a template that +renders the login form and (3) a :doc:`Guard authenticator ` +class that processes the login submit. + +The ``/login`` route & controller:: // src/Controller/SecurityController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; + use Symfony\Component\HttpFoundation\Response; + use Symfony\Component\Routing\Annotation\Route; + use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class SecurityController extends AbstractController { - } - -Next, configure the route that you earlier used under your ``form_login`` -configuration (``login``): - -.. configuration-block:: - - .. code-block:: php-annotations - - // src/Controller/SecurityController.php - - // ... - use Symfony\Component\Routing\Annotation\Route; - - class SecurityController extends AbstractController + /** + * @Route("/login", name="app_login") + */ + public function login(AuthenticationUtils $authenticationUtils): Response { - /** - * @Route("/login", name="login") - */ - public function login() - { - } + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error + ]); } - - .. code-block:: yaml - - # config/routes.yaml - login: - path: /login - controller: App\Controller\SecurityController::login - - .. code-block:: xml - - - - - - - App\Controller\SecurityController::login - - - - .. code-block:: php - - // config/routes.php - use App\Controller\SecurityController; - use Symfony\Component\Routing\RouteCollection; - use Symfony\Component\Routing\Route; - - $routes = new RouteCollection(); - $routes->add('login', new Route('/login', array( - '_controller' => array(SecurityController::class, 'login'), - ))); - - return $routes; - -Great! Next, add the logic to ``login()`` that displays the login form:: - - // src/Controller/SecurityController.php - use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; - - public function login(AuthenticationUtils $authenticationUtils) - { - // get the login error if there is one - $error = $authenticationUtils->getLastAuthenticationError(); - - // last username entered by the user - $lastUsername = $authenticationUtils->getLastUsername(); - - return $this->render('security/login.html.twig', array( - 'last_username' => $lastUsername, - 'error' => $error, - )); } -.. note:: - - If you get an error that the ``$authenticationUtils`` argument is missing, - it's probably because the controllers of your application are not defined as - services and tagged with the ``controller.service_arguments`` tag, as done - in the :ref:`default services.yaml configuration `. - -Don't let this controller confuse you. As you'll see in a moment, when the -user submits the form, the security system automatically handles the form -submission for you. If the user submits an invalid username or password, -this controller reads the form submission error from the security system, -so that it can be displayed back to the user. +The template has very little to do with security: it just generates a traditional +HTML form that submits to ``/login``: -In other words, your job is to *display* the login form and any login errors -that may have occurred, but the security system itself takes care of checking -the submitted username and password and authenticating the user. +.. code-block:: twig -Finally, create the template: + {% extends 'base.html.twig' %} -.. code-block:: html+twig + {% block title %}Log in!{% endblock %} - {# templates/security/login.html.twig #} - {# ... you will probably extend your base template, like base.html.twig #} + {% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} - {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} +

Please sign in

+ + + + - - - - - - + {# - If you want to control the URL the user - is redirected to on success (more details below) - + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ +
#} - +
+ {% endblock %} + +The Guard authenticator processes the form submit:: + + // src/Security/LoginFormAuthenticator.php + namespace App\Security; + + use App\Entity\User; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\RedirectResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Routing\RouterInterface; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; + use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; + use Symfony\Component\Security\Core\Security; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\UserProviderInterface; + use Symfony\Component\Security\Csrf\CsrfToken; + use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; + use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; + use Symfony\Component\Security\Http\Util\TargetPathTrait; + + class LoginFormAuthenticator extends AbstractFormLoginAuthenticator + { + use TargetPathTrait; -.. tip:: + private $entityManager; + private $router; + private $csrfTokenManager; + private $passwordEncoder; - The ``error`` variable passed into the template is an instance of - :class:`Symfony\\Component\\Security\\Core\\Exception\\AuthenticationException`. - It may contain more information - or even sensitive information - about - the authentication failure, so use it wisely! + public function __construct(EntityManagerInterface $entityManager, RouterInterface $router, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) + { + $this->entityManager = $entityManager; + $this->router = $router; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } -The form can look like anything, but it usually follows some conventions: + public function supports(Request $request) + { + return 'app_login' === $request->attributes->get('_route') + && $request->isMethod('POST'); + } -* The ``
`` element sends a ``POST`` request to the ``login`` route, since - that's what you configured under the ``form_login`` key in ``security.yaml``; -* The username field has the name ``_username`` and the password field has the - name ``_password``. + public function getCredentials(Request $request) + { + $credentials = [ + 'email' => $request->request->get('email'), + 'password' => $request->request->get('password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + $request->getSession()->set( + Security::LAST_USERNAME, + $credentials['email'] + ); + + return $credentials; + } -.. tip:: + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } - Actually, all of this can be configured under the ``form_login`` key. See - :ref:`reference-security-firewall-form-login` for more details. + $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); -.. caution:: + if (!$user) { + // fail authentication with a custom error + throw new CustomUserMessageAuthenticationException('Email could not be found.'); + } - This login form is currently not protected against CSRF attacks. Read - :doc:`/security/csrf` on how to protect your login form. + return $user; + } -And that's it! When you submit the form, the security system will automatically -check the user's credentials and either authenticate the user or send the -user back to the login form where the error can be displayed. + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } -To review the whole process: + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + return new RedirectResponse($targetPath); + } -#. The user tries to access a resource that is protected; -#. The firewall initiates the authentication process by redirecting the - user to the login form (``/login``); -#. The ``/login`` page renders login form via the route and controller created - in this example; -#. The user submits the login form to ``/login``; -#. The security system intercepts the request, checks the user's submitted - credentials, authenticates the user if they are correct, and sends the - user back to the login form if they are not. + // For example : return new RedirectResponse($this->router->generate('some_route')); + throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + } -Redirecting after Success -------------------------- + protected function getLoginUrl() + { + return $this->router->generate('app_login'); + } + } -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, they'll be redirected to the homepage. This -can all be customized, allowing you to, for example, redirect the user to -a specific URL. +Finishing the Login Form +------------------------ -For more details on this and how to customize the form login process in general, -see :doc:`/security/form_login`. +Woh. The ``make:auth`` command just did a *lot* of work for you. But, you're not done +yet. First, go to ``/login`` to see the new login form. Feel free to customize this +however you want. -.. _security-common-pitfalls: +When you submit the form, the ``LoginFormAuthenticator`` will intercept the request, +read the email (or whatever field you're using) & password from the form, find the +``User`` object, validate the CSRF token and check the password. -Avoid Common Pitfalls ---------------------- +But, wepending on your setup, you'll need to finish one or more TODOs before the +whole process works. You will *at least* need to fill in *where* you want your user to +be redirected after success: -When setting up your login form, watch out for a few common pitfalls. +.. code-block:: diff -1. Create the Correct Routes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // src/Security/LoginFormAuthenticator.php -First, be sure that you've defined the ``/login`` route correctly and that -it corresponds to the ``login_path`` and ``check_path`` config values. -A misconfiguration here can mean that you're redirected to a 404 page instead -of the login page, or that submitting the login form does nothing (you just see -the login form over and over again). + // ... + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + // ... -2. Be Sure the Login Page Isn't Secure (Redirect Loop!) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + - throw new \Exception('TODO: provide a valid redirect inside '.__FILE__); + + // redirect to some "app_homepage" route - of wherever you want + + return new RedirectResponse($this->router->generate('app_homepage')); + } -Also, be sure that the login page is accessible by anonymous users. For example, -the following configuration - which requires the ``ROLE_ADMIN`` role for -all URLs (including the ``/login`` URL), will cause a redirect loop: +Unless you have any other TODOs in that file, that's it! If you're loading users +from the database, make sure you've loaded some :ref:`dummy users `. +Then, try to login. -.. configuration-block:: +If you're successful, the web debug toolbar will tell you who you are and what roles +you have: - .. code-block:: yaml +.. image:: /_images/security/symfony_loggedin_wdt.png + :align: center - # config/packages/security.yaml +The Guard authentication system is powerful, and you can customize your authenticator +class to do whatever you need. To learn more about what the individual methods do, +see :doc:`/security/guard_authentication`. - # ... - access_control: - - { path: ^/, roles: ROLE_ADMIN } +Controlling Error Messages +-------------------------- - .. code-block:: xml +You can cause authentication to fail with a custom message at any step by throwing +a custom :class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. +This is an easy way to control the error message. - - - +But in some cases, like if you return ``false`` from ``checkCredentials()``, you +may see an error that comes from the core of Symfony - like ``Invalid credentials.``. - - - - - +To customize this message, you could throw a ``CustomUserMessageAuthenticationException`` +instead. Or, you can :doc:`translate ` the message through the ``security`` +domain: - .. code-block:: php +.. configuration-block:: - // config/packages/security.php + .. code-block:: xml - // ... - 'access_control' => array( - array('path' => '^/', 'role' => 'ROLE_ADMIN'), - ), + + + + + + + Invalid credentials. + The password you entered was invalid! + + + + -Adding an access control that matches ``/login/*`` and requires *no* authentication -fixes the problem: + .. code-block:: yaml -.. configuration-block:: + # translations/security.en.yaml + 'Invalid credentials.': 'The password you entered was invalid!' - .. code-block:: yaml + .. code-block:: php - # config/packages/security.yaml + // translations/security.en.php + return array( + 'Invalid credentials.' => 'The password you entered was invalid!', + ); - # ... - access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/, roles: ROLE_ADMIN } +If the message isn't translated, make sure you've installed the ``translator`` +and try clearing your cache: - .. code-block:: xml +.. code-block:: terminal - - - - - - - - - - + $ php bin/console cache:clear - .. code-block:: php +Redirecting to the Last Accessed Page with ``TargetPathTrait`` +-------------------------------------------------------------- - // config/packages/security.php +The last request URI is stored in a session variable named +``_security..target_path`` (e.g. ``_security.main.target_path`` +if the name of your firewall is ``main``). Most of the times you don't have to +deal with this low level session variable. However, the +:class:`Symfony\\Component\\Security\\Http\\Util\\TargetPathTrait` utility +can be used to read (like in the example above) or set this value manually. - // ... - 'access_control' => array( - array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'), - array('path' => '^/', 'role' => 'ROLE_ADMIN'), - ), - -3. Be Sure check_path Is Behind a Firewall -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Next, make sure that your ``check_path`` URL (e.g. ``/login``) is behind -the firewall you're using for your form login (in this example, the single -firewall matches *all* URLs, including ``/login``). If ``/login`` -doesn't match any firewall, you'll receive a ``Unable to find the controller -for path "/login"`` exception. - -4. Multiple Firewalls Don't Share the Same Security Context -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you're using multiple firewalls and you authenticate against one firewall, -you will *not* be authenticated against any other firewalls automatically. -Different firewalls are like different security systems. To do this you have -to explicitly specify the same :ref:`reference-security-firewall-context` -for different firewalls. But usually for most applications, having one -main firewall is enough. - -5. Routing Error Pages Are not Covered by Firewalls -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -As routing is done *before* security, 404 error pages are not covered by -any firewall. This means you can't check for security or even access the -user object on these pages. See :doc:`/controller/error_pages` -for more details. - -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle +.. _`MakerBundle`: https://github.com/symfony/maker-bundle diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst index 7f5cf7af22d..416eda65c8a 100644 --- a/security/guard_authentication.rst +++ b/security/guard_authentication.rst @@ -1,167 +1,71 @@ .. index:: single: Security; Custom Authentication -How to Create a Custom Authentication System with Guard -======================================================= +Custom Authentication System with Guard (API Token Example) +=========================================================== Whether you need to build a traditional login form, an API token authentication system or you need to integrate with some proprietary single-sign-on system, the Guard component can make it easy... and fun! -In this example, you'll build an API token authentication system and learn how -to work with Guard. +Guard authentication can easily be used to -Create a User and a User Provider ---------------------------------- +* :doc:`Build a Login Form `, +* Create an API token authentication system (done on this page!) +* `Social Authentication`_ (or use `HWIOAuthBundle`_ for a robust, but non-Guard solution) -No matter how you authenticate, you need to create a User class that implements ``UserInterface`` -and configure a :doc:`user provider `. In this -example, users are stored in the database via Doctrine, and each user has an ``apiKey`` -property they use to access their account via the API:: +In this example, we'll build an API token authentication system so we can learn +more about Guard in detail. - // src/Entity/User.php - namespace App\Entity; - - use Symfony\Component\Security\Core\User\UserInterface; - use Doctrine\ORM\Mapping as ORM; +Step 0) Prepare your User Class +------------------------------- - /** - * @ORM\Entity - * @ORM\Table(name="`user`") - */ - class User implements UserInterface - { - /** - * @ORM\Id - * @ORM\GeneratedValue(strategy="AUTO") - * @ORM\Column(type="integer") - */ - private $id; +Suppose you want to build an API where your clients will send an ``X-AUTH-TOKEN`` header +on each request with their API token. Your job is to read this and find the associated +user (if any). - /** - * @ORM\Column(type="string", unique=true) - */ - private $username; +First, make sure you've followed the main :doc:`Security Guide ` to +create your ``User`` class. Then, to keep things simple, add an ``apiToken`` property +directly to your ``User`` class (the ``make:entity`` command is a good way to do this): - /** - * @ORM\Column(type="string", unique=true) - */ - private $apiKey; +.. code-block:: diff - public function getUsername() - { - return $this->username; - } + // src/Entity/User.php + // ... - public function getRoles() - { - return array('ROLE_USER'); - } + class User implements UserInterface + { + // ... - public function getPassword() - { - } - public function getSalt() - { - } - public function eraseCredentials() - { - } + + /** + + * @ORM\Column(type="string", unique=true) + + */ + + private $apiToken; - // more getters/setters + // the getter and setter methods } -.. caution:: - - In the example above, the table name is ``user``. This is a reserved SQL - keyword and `must be quoted with backticks`_ in Doctrine to avoid errors. - You might also change the table name (e.g. with ``app_users``) to solve - this issue. - -.. tip:: - - This User doesn't have a password, but you can add a ``password`` property if - you also want to allow this user to login with a password (e.g. via a login form). - -Your ``User`` class doesn't need to be stored in Doctrine: do whatever you need. -Next, make sure you've configured a "user provider" for the user: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - # ... +Don't forget to generate and execute the migration: - providers: - your_db_provider: - entity: - class: App\Entity\User - property: apiKey +.. code-block:: terminal - # ... - - .. code-block:: xml - - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', array( - // ... - - 'providers' => array( - 'your_db_provider' => array( - 'entity' => array( - 'class' => User::class, - ), - ), - ), - - // ... - )); - -That's it! Need more information about this step, see: - -* :doc:`/security/entity_provider` -* :doc:`/security/custom_provider` + $ php bin/console make:migration + $ php bin/console doctrine:migrations:migrate Step 1) Create the Authenticator Class -------------------------------------- -Suppose you have an API where your clients will send an ``X-AUTH-TOKEN`` header -on each request with their API token. Your job is to read this and find the associated -user (if any). - To create a custom authentication system, just create a class and make it implement :class:`Symfony\\Component\\Security\\Guard\\AuthenticatorInterface`. Or, extend the simpler :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`. + This requires you to implement several methods:: // src/Security/TokenAuthenticator.php namespace App\Security; + use App\Entity\User; + use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; @@ -173,6 +77,13 @@ This requires you to implement several methods:: class TokenAuthenticator extends AbstractGuardAuthenticator { + private $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + /** * Called on every request to decide if this authenticator should be * used for the request. Returning false will cause this authenticator @@ -196,14 +107,15 @@ This requires you to implement several methods:: public function getUser($credentials, UserProviderInterface $userProvider) { - $apiKey = $credentials['token']; + $apiToken = $credentials['token']; - if (null === $apiKey) { + if (null === $apiToken) { return; } // if a User object, checkCredentials() is called - return $userProvider->loadUserByUsername($apiKey); + return $this->em->getRepository(User::class) + ->findOneBy(['apiToken' => $apiToken]); } public function checkCredentials($credentials, UserInterface $user) @@ -252,6 +164,7 @@ This requires you to implement several methods:: } } + Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods`. Step 2) Configure the Authenticator @@ -285,7 +198,6 @@ Finally, configure your ``firewalls`` key in ``security.yaml`` to use this authe # if you want, disable storing the user in the session # stateless: true - # maybe other things, like form_login, remember_me, etc # ... .. code-block:: xml @@ -476,58 +388,37 @@ egg to return a custom message if someone tries this: curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/ # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"} -Building a Login Form ---------------------- - -If you're building a login form, use the :class:`Symfony\\Component\\Security\\Guard\\Authenticator\\AbstractFormLoginAuthenticator` -as your base class - it implements a few methods for you. Then, fill in the other -methods just like with the ``TokenAuthenticator``. Outside of Guard, you are still -responsible for creating a route, controller and template for your login form. - -.. _guard-csrf-protection: +.. _guard-manual-auth: -Adding CSRF Protection ----------------------- +Manually Authenticating a User +------------------------------ -If you're using a Guard authenticator to build a login form and want to add CSRF -protection, no problem! +Sometimes you might want to manually authenticate a user - like after the user +completes registration. To do that, use your authenticator and a service called +``GuardAuthenticatorHandler``:: -First, check that :ref:`the csrf_protection option ` -is enabled and :ref:`add the _csrf_token field to your login form `. - -Then, type-hint ``CsrfTokenManagerInterface`` in your ``__construct()`` method -(or manually configure the ``Symfony\Component\Security\Csrf\CsrfTokenManagerInterface`` -service to be passed) and add the following logic:: - - // src/Security/ExampleFormAuthenticator.php + // src/Controller/RegistrationController.php // ... - use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; - use Symfony\Component\Security\Csrf\CsrfToken; - use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; - use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; + use App\Security\LoginFormAuthenticator; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; - class ExampleFormAuthenticator extends AbstractFormLoginAuthenticator + class RegistrationController extends AbstractController { - private $csrfTokenManager; - - public function __construct(CsrfTokenManagerInterface $csrfTokenManager) - { - $this->csrfTokenManager = $csrfTokenManager; - } - - public function getCredentials(Request $request) + public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $reques) { - $csrfToken = $request->request->get('_csrf_token'); - - if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate', $csrfToken))) { - throw new InvalidCsrfTokenException('Invalid CSRF token.'); - } - - // ... all your normal logic + // ... + // after validating the user and saving them to the database + + // authenticate the user and use onAuthenticationSuccess on the authenticator + return $guardHandler->authenticateUserAndHandleSuccess( + $user, // the User object you just created + $request, + $authenticator, // authenticator whose onAuthenticationSuccess you want to use + 'main' // the name if your firewall in security.yaml + ); } - - // ... } Avoid Authenticating the Browser on Every Request @@ -622,3 +513,5 @@ Frequently Asked Questions authenticator(s) (just like in this article). .. _`must be quoted with backticks`: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#quoting-reserved-words +.. _`Social Authentication`: https://github.com/knpuniversity/oauth2-client-bundle#authenticating-with-guard +.. _`HWIOAuthBundle`: https://github.com/hwi/HWIOAuthBundle diff --git a/security/host_restriction.rst b/security/host_restriction.rst deleted file mode 100644 index 0a07e4f6262..00000000000 --- a/security/host_restriction.rst +++ /dev/null @@ -1,5 +0,0 @@ -How to Restrict Firewalls to a Specific Host -============================================ - -There are more possibilities to restrict firewalls. You can read everything -about all the possibilities (including ``host``) in ":doc:`/security/firewall_restriction`". diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index e2d901dd0e0..b64f2d220a8 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -5,16 +5,14 @@ How to Impersonate a User ========================= Sometimes, it's useful to be able to switch from one user to another without -having to log out and log in again (for instance when you are debugging or trying -to understand a bug a user sees that you can't reproduce). +having to log out and log in again (for instance when you are debugging something +a user sees that you can't reproduce). .. caution:: - User impersonation is not compatible with - :doc:`pre authenticated firewalls `. The - reason is that impersonation requires the authentication state to be maintained - server-side, but pre-authenticated information (``SSL_CLIENT_S_DN_Email``, - ``REMOTE_USER`` or other) is sent in each request. + User impersonation is not compatible with some authentication mechanisms + (e.g. ``REMOTE_USER``) where the authentication information is expected to be + sent on each request. Impersonating the user can be easily done by activating the ``switch_user`` firewall listener: @@ -67,7 +65,8 @@ firewall listener: )); To switch to another user, just add a query string with the ``_switch_user`` -parameter and the username as the value to the current URL: +parameter and the username (or whatever field our user provider uses to load users) +as the value to the current URL: .. code-block:: text @@ -79,6 +78,13 @@ To switch back to the original user, use the special ``_exit`` username: http://example.com/somewhere?_switch_user=_exit +This feature is only available to users with a special role called ``ROLE_ALLOWED_TO_SWITCH``. +Using :ref:`role_hierarchy` is a great way to give this +role to the users that need it. + +Knowing When Impersonation Is Active +------------------------------------ + During impersonation, the user is provided with a special role called ``ROLE_PREVIOUS_ADMIN``. In a template, for instance, this role can be used to show a link to exit impersonation: @@ -89,6 +95,9 @@ to show a link to exit impersonation: Exit impersonation {% endif %} +Finding the Original User +------------------------- + In some cases you may need to get the object that represents the impersonator user rather than the impersonated user. Use the following snippet to iterate over the user's roles until you find one that is a ``SwitchUserRole`` object:: @@ -121,6 +130,9 @@ over the user's roles until you find one that is a ``SwitchUserRole`` object:: } } +Controlling the Query Parameter +------------------------------- + Of course, this feature needs to be made available to a small group of users. By default, access is restricted to users having the ``ROLE_ALLOWED_TO_SWITCH`` role. The name of this role can be modified via the ``role`` setting. For diff --git a/security/ldap.rst b/security/ldap.rst index 64167b42f9a..563d21ca3c4 100644 --- a/security/ldap.rst +++ b/security/ldap.rst @@ -8,7 +8,7 @@ Symfony provides different means to work with an LDAP server. The Security component offers: -* The ``ldap`` user provider, using the +* The ``ldap`` :doc:`user provider`, using the :class:`Symfony\\Component\\Security\\Core\\User\\LdapUserProvider` class. Like all other user providers, it can be used with any authentication provider. diff --git a/security/multiple_user_providers.rst b/security/multiple_user_providers.rst deleted file mode 100644 index c2a6ca3b550..00000000000 --- a/security/multiple_user_providers.rst +++ /dev/null @@ -1,177 +0,0 @@ -How to Use multiple User Providers -================================== - -.. note:: - - It's always better to use a specific user provider for each authentication - mechanism. Chaining user providers should be avoided in most applications - and used only to solve edge cases. - -Each authentication mechanism (e.g. HTTP Authentication, form login, etc.) uses -exactly one user provider. But what if you want to specify a few users via -configuration and the rest of your users in the database? This is possible by -creating a new provider that chains the two together: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - providers: - chain_provider: - chain: - providers: [in_memory, user_db] - in_memory: - memory: - users: - foo: { password: test } - user_db: - entity: { class: App\Entity\User, property: username } - - .. code-block:: xml - - - - - - - - - in_memory - user_db - - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - use App\Entity\User; - - $container->loadFromExtension('security', array( - 'providers' => array( - 'chain_provider' => array( - 'chain' => array( - 'providers' => array('in_memory', 'user_db'), - ), - ), - 'in_memory' => array( - 'memory' => array( - 'users' => array( - 'foo' => array('password' => 'test'), - ), - ), - ), - 'user_db' => array( - 'entity' => array( - 'class' => User::class, - 'property' => 'username', - ), - ), - ), - )); - -Now, all firewalls that define ``chain_provider`` as their user provider will, -in turn, try to load the user from both the ``in_memory`` and ``user_db`` -providers. - -You can also configure the firewall or individual authentication mechanisms -to use a specific provider: - -.. configuration-block:: - - .. code-block:: yaml - - # config/packages/security.yaml - security: - firewalls: - secured_area: - # ... - pattern: ^/ - provider: user_db - http_basic: - realm: 'Secured Demo Area' - provider: in_memory - form_login: ~ - - .. code-block:: xml - - - - - - - - - - - - - - - .. code-block:: php - - // config/packages/security.php - $container->loadFromExtension('security', array( - 'firewalls' => array( - 'secured_area' => array( - // ... - 'pattern' => '^/', - 'provider' => 'user_db', - 'http_basic' => array( - // ... - 'realm' => 'Secured Demo Area', - 'provider' => 'in_memory', - ), - 'form_login' => array(), - ), - ), - )); - -In this example, if a user tries to log in via HTTP authentication, the -authentication system will use the ``in_memory`` user provider. But if the user -tries to log in via the form login, the ``user_db`` provider will be used (since -it's the default for the firewall as a whole). - -If you need to check that the user being returned by your provider is a allowed -to authenticate, check the returned user object:: - - use Symfony\Component\Security\Core\User; - // ... - - public function loadUserByUsername($username) - { - // ... - - // you can, for example, test that the returned user is an object of a - // particular class or check for certain attributes of your user objects - if ($user instance User) { - // the user was loaded from the main security config file. Do something. - // ... - } - - return $user; - } - -For more information about user provider and firewall configuration, see -the :doc:`/reference/configuration/security`. diff --git a/security/named_encoders.rst b/security/named_encoders.rst index f9137037b94..7826fe865f9 100644 --- a/security/named_encoders.rst +++ b/security/named_encoders.rst @@ -1,8 +1,8 @@ .. index:: single: Security; Named Encoders -How to Choose the Password Encoder Algorithm Dynamically -======================================================== +How to Use A Different Password Encoder Algorithm Per User +========================================================== Usually, the same password encoder is used for all users by configuring it to apply to all instances of a specific class: @@ -15,7 +15,8 @@ to apply to all instances of a specific class: security: # ... encoders: - Symfony\Component\Security\Core\User\User: sha512 + App\Entity\User: + algorithm: bcrypt .. code-block:: xml @@ -29,8 +30,8 @@ to apply to all instances of a specific class: > - @@ -38,13 +39,13 @@ to apply to all instances of a specific class: .. code-block:: php // config/packages/security.php - use Symfony\Component\Security\Core\User\User; + use App\Entity\User; $container->loadFromExtension('security', array( // ... 'encoders' => array( User::class => array( - 'algorithm' => 'sha512', + 'algorithm' => 'bcrypt', ), ), )); @@ -52,10 +53,10 @@ to apply to all instances of a specific class: Another option is to use a "named" encoder and then select which encoder you want to use dynamically. -In the previous example, you've set the ``sha512`` algorithm for ``Acme\UserBundle\Entity\User``. +In the previous example, you've set the ``bcrypt`` algorithm for ``App\Entity\User``. This may be secure enough for a regular user, but what if you want your admins -to have a stronger algorithm, for example ``bcrypt``. This can be done with -named encoders: +to have a stronger algorithm, for example ``bcrypt`` with a higher cost. This can +be done with named encoders: .. configuration-block:: diff --git a/security/password_encoding.rst b/security/password_encoding.rst deleted file mode 100644 index 2a3c6509eb8..00000000000 --- a/security/password_encoding.rst +++ /dev/null @@ -1,45 +0,0 @@ -.. index:: - single: Security; Encoding Passwords - -How to Manually Encode a Password -================================= - -.. note:: - - For historical reasons, Symfony uses the term *"password encoding"* when it - should really refer to *"password hashing"*. The "encoders" are in fact - `cryptographic hash functions`_. - -If, for example, you're storing users in the database, you'll need to encode -the users' passwords before inserting them. No matter what algorithm you -configure for your user object, the hashed password can always be determined -in the following way from a controller:: - - use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; - - public function register(UserPasswordEncoderInterface $encoder) - { - // whatever *your* User object is - $user = new App\Entity\User(); - $plainPassword = 'ryanpass'; - $encoded = $encoder->encodePassword($user, $plainPassword); - - $user->setPassword($encoded); - } - -In order for this to work, just make sure that you have the encoder for your -user class (e.g. ``App\Entity\User``) configured under the ``encoders`` -key in ``config/packages/security.yaml``. - -The ``$encoder`` object also has an ``isPasswordValid()`` method, which takes -the ``User`` object as the first argument and the plain password to check -as the second argument. - -.. caution:: - - When you allow a user to submit a plaintext password (e.g. registration - form, change password form), you *must* have validation that guarantees - that the password is 4096 characters or fewer. Read more details in - :ref:`How to implement a simple Registration Form `. - -.. _`cryptographic hash functions`: https://en.wikipedia.org/wiki/Cryptographic_hash_function diff --git a/security/remember_me.rst b/security/remember_me.rst index 89bda228493..a70c8d332d5 100644 --- a/security/remember_me.rst +++ b/security/remember_me.rst @@ -146,21 +146,14 @@ this: .. code-block:: html+twig {# templates/security/login.html.twig #} - {% if error %} -
{{ error.message }}
- {% endif %} - - - - - - + + {# ... your form fields #} - + {# ... #} The user will then automatically be logged in on subsequent visits while @@ -176,90 +169,26 @@ visiting the site. In some cases, however, you may want to force the user to actually re-authenticate before accessing certain resources. For example, you might allow "remember me" -users to see basic account information, but then require them to actually -re-authenticate before modifying that information. - -The Security component provides an easy way to do this. In addition to roles -explicitly assigned to them, users are automatically given one of the following -roles depending on how they are authenticated: - -``IS_AUTHENTICATED_ANONYMOUSLY`` - Automatically assigned to a user who is in a firewall protected part of the - site but who has not actually logged in. This is only possible if anonymous - access has been allowed. - -``IS_AUTHENTICATED_REMEMBERED`` - Automatically assigned to a user who was authenticated via a remember me - cookie. - -``IS_AUTHENTICATED_FULLY`` - Automatically assigned to a user that has provided their login details - during the current session. - -You can use these to control access beyond the explicitly assigned roles. - -.. note:: - - If you have the ``IS_AUTHENTICATED_REMEMBERED`` role, then you also - have the ``IS_AUTHENTICATED_ANONYMOUSLY`` role. If you have the ``IS_AUTHENTICATED_FULLY`` - role, then you also have the other two roles. In other words, these roles - represent three levels of increasing "strength" of authentication. - -You can use these additional roles for finer grained control over access to -parts of a site. For example, you may want your user to be able to view their -account at ``/account`` when authenticated by cookie but to have to provide -their login details to be able to edit the account details. You can do this -by securing specific controller actions using these roles. The edit action -in the controller could be secured using the service context. - -In the following example, the action is only allowed if the user has the -``IS_AUTHENTICATED_FULLY`` role:: - - // ... - use Symfony\Component\Security\Core\Exception\AccessDeniedException; +users to change their password. You can do this by leveraing a few special "roles":: + // src/Controller/AccountController.php // ... - public function edit() + + public function accountInfo() { - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + // allow any authenticated user - we don't care if they just + // logged in, or are logged in via a remember me cookie + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); // ... } -If you have installed `SensioFrameworkExtraBundle`_ in your application, you can also secure -your controller using annotations:: - - use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; - - /** - * @Security("is_granted('IS_AUTHENTICATED_FULLY')") - */ - public function edit($name) + public function resetPassword() { + // require the user to log in during *this* session + // if they were only logged in via a remember me cookie, they + // will be redirected to the login page + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + // ... } - -.. tip:: - - If you also had an access control in your security configuration that - required the user to have a ``ROLE_USER`` role in order to access any - of the account area, then you'd have the following situation: - - * If a non-authenticated (or anonymously authenticated user) tries to - access the account area, the user will be asked to authenticate. - - * Once the user has entered their username and password, assuming the - user receives the ``ROLE_USER`` role per your configuration, the user - will have the ``IS_AUTHENTICATED_FULLY`` role and be able to access - any page in the account section, including the ``edit()`` controller. - - * If the user's session ends, when the user returns to the site, they will - be able to access every account page - except for the edit page - without - being forced to re-authenticate. However, when they try to access the - ``edit()`` controller, they will be forced to re-authenticate, since - they are not, yet, fully authenticated. - -For more information on securing services or methods in this way, -see :doc:`/security/securing_services`. - -.. _`SensioFrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html diff --git a/security/securing_services.rst b/security/securing_services.rst index cbfcbd62437..14341166150 100644 --- a/security/securing_services.rst +++ b/security/securing_services.rst @@ -5,68 +5,37 @@ How to Secure any Service or Method in your Application ======================================================= -In the security article, you can see how to -:ref:`secure a controller ` by requesting -the ``Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface`` -service from the Service Container and checking the current user's role:: +In the security article, you learned how to +:ref:`secure a controller ` via a shortcut method. - // ... - use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; - use Symfony\Component\Security\Core\Exception\AccessDeniedException; - - public function hello(AuthorizationCheckerInterface $authChecker) - { - if (!$authChecker->isGranted('ROLE_ADMIN')) { - throw new AccessDeniedException(); - } - - // ... - } - -You can also secure *any* service by injecting the authorization checker -service into it. For a general introduction to injecting dependencies into -services see the :doc:`/service_container` article. For example, suppose you -have a ``NewsletterManager`` class that sends out emails and you want to -restrict its use to only users who have some ``ROLE_NEWSLETTER_ADMIN`` role. -Before you add security, the class looks something like this:: - - // src/Newsletter/NewsletterManager.php - namespace App\Newsletter; +But, you can check access *anywhere* in your code by injection the ``Security`` +service. For example, suppose you have a ``SalesReportManager`` service and you +want to include extra details onlt for users that have some ``ROLE_SALES_ADMIN`` role: - class NewsletterManager - { - public function sendNewsletter() - { - // ... where you actually do the work - } - - // ... - } - -Your goal is to check the user's role when the ``sendNewsletter()`` method is -called. The first step towards this is to inject the ``security.helper`` service -using the :class:`Symfony\\Component\\Security\\Core\\Security` class:: +.. code-block:: diff // src/Newsletter/NewsletterManager.php // ... use Symfony\Component\Security\Core\Exception\AccessDeniedException; - use Symfony\Component\Security\Core\Security; + + use Symfony\Component\Security\Core\Security; - class NewsletterManager + class SalesReportManager { - protected $security; + + private $security; - public function __construct(Security $security) - { - $this->security = $security; - } + + public function __construct(Security $security) + + { + + $this->security = $security; + + } public function sendNewsletter() { - if (!$this->security->isGranted('ROLE_NEWSLETTER_ADMIN')) { - throw new AccessDeniedException(); - } + $salesData = []; + + + if ($this->security->isGranted('ROLE_SALES_ADMIN')) { + + $salesData['top_secret_numbers'] = rand(); + + } // ... } @@ -78,5 +47,7 @@ If you're using the :ref:`default services.yaml configuration `, use the user provider + to load a User object via is "username" (or email, or whatever field you want). + +Symfony comes with several built-in user providers: + +.. toctree:: + :hidden: + + entity_provider + +* :doc:`entity: (load users from the database) ` +* :doc:`ldap ` +* ``memory`` (users are hardcoded in config) +* ``chain`` (try multiple user providers) + +Or you can create a :ref:`custom user provider `. + +User providers are configured in ``config/packages/security.yaml`` under the +``providers`` key, and each has different configuration options: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + # this becomes the internal name of the provider + # not usually important, but can be used to specify which + # provider you want for which firewall (advanced case) or + # for a specific authentication provider + some_provider_key: + + # provider type - one of the above + memory: + # custom options for that provider + users: + user: { password: userpass, roles: [ 'ROLE_USER' ] } + admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } + + a_chain_provider: + chain: + providers: [some_provider_key, another_provider_key] + +.. _custom-user-provider: + +Creating a Custom User Provider +------------------------------- + +If you're loading users from a custom location (e.g. via an API or legacy database +connection), you'll need to create a custom user provider class. First, make sure +you've followed the :doc:`Security Guide ` to create your ``User`` class. + +If you used the ``make:user`` command to create your ``User`` class (and you answered +the questions indicating that you need a custom user provider), that command will +generate a nice skeleton to get you started:: + + // .. src/Security/UserProvider.php + namespace App\Security; + + use Symfony\Component\Security\Core\Exception\UnsupportedUserException; + use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Core\User\UserProviderInterface; + + class UserProvider implements UserProviderInterface + { + /** + * Symfony calls this method if you use features like switch_user + * or remember_me. + * + * If you're not using these features, you do not need to implement + * this method. + * + * @return UserInterface + */ + public function loadUserByUsername($username) + { + // Load a User object from your data source or throw UsernameNotFoundException. + // The $username argument may not actually be a username: + // it is whatever value is being returned by the getUsername() + // method in your User class. + throw new \Exception('TODO: fill in loadUserByUsername() inside '.__FILE__); + } + + /** + * Refreshes the user after being reloaded from the session. + * + * When a user is logged in, at the beginning of each request, the + * User object is loaded from the session and then this method is + * called. Your job is to make sure the user's data is still fresh by, + * for example, re-querying for fresh User data. + * + * If your firewall is "stateless: false" (for a pure API), this + * method is not called. + * + * @return UserInterface + */ + public function refreshUser(UserInterface $user) + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); + } + + /* @var User $user */ + + // Return a User object after making sure its data is "fresh". + // Or throw a UsernameNotFoundException if the user no longer exists. + throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); + } + + /** + * Tells Symfony to use this provider for this User class. + */ + public function supportsClass($class) + { + return User::class === $class; + } + } + +Most of the work is already done! Read the comments in the code and update the TODO +sections to finish the user provider. + +When you're done, tell Symfony about the user provider by adding it in ``security.yaml``: + +.. code-block:: yaml + + # config/packages/security.yaml + security: + providers: + # internal name - can be anything + your_custom_user_provider: + id: App\Security\UserProvider + +That's it! When you use any of the features that require a user provider, your +provider will be used! If you have multiple firewalls and multiple providers, +you can specify *which* provider to use by adding a ``provider`` key under your +firewall and setting it to the internal name you gave to your user provider. + +.. _user_session_refresh: + +Understanding how Users are Refreshed from the Session +------------------------------------------------------ + +At the end of every request (unless your firewall is ``stateless``), your ``User`` +object is serialized to the session. At the beginning of the next request, it's +deserialized and then passed to your user provider to "refresh" it (e.g. Doctrine +queries for a fresh user). + +Then, the two User objects (the original from the session and the refreshed User +object) are "compared" to see if they are "equal". By default, the core +``AbstractToken`` class compares the return values of the ``getPassword()``, +``getSalt()`` and ``getUsername()`` methods. If any of these are different, your +user will be logged out. This is a security measure to make sure that malicious +users can be de-authenticated if core user data changes. + +However, in some cases, this process can cause unexpected authentication problems. +If you're having problems authenticating, it could be that you *are* authenticating +successfully, but you immediately lose authentication after the first redirect. + +In that case, review the serialization logic (e.g. ``SerializableInterface``) if +you have any, to make sure that all the fields necessary are serialized. + +Comparing Users Manually with EquatableInterface +------------------------------------------------ + +Or, if you need more control over the "compare users" process, make your User class +implement :class:`Symfony\\Component\\Security\\Core\\User\\EquatableInterface`. +Then, your ``isEqualTo()`` method will be called when comparing users. + diff --git a/security/voters.rst b/security/voters.rst index 5fc6022d9f0..5fcbcc0a7c4 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -207,14 +207,14 @@ Checking for Roles inside a Voter What if you want to call ``isGranted()`` from *inside* your voter - e.g. you want to see if the current user has ``ROLE_SUPER_ADMIN``. That's possible by injecting -the :class:`Symfony\\Component\\Security\\Core\\Authorization\\AccessDecisionManager` +the :class:`Symfony\\Component\\Security\\Core\\Security` into your voter. You can use this to, for example, *always* allow access to a user with ``ROLE_SUPER_ADMIN``:: // src/Security/PostVoter.php // ... - use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; + use Symfony\Component\Security\Core\Security; class PostVoter extends Voter { @@ -222,9 +222,9 @@ with ``ROLE_SUPER_ADMIN``:: private $decisionManager; - public function __construct(AccessDecisionManagerInterface $decisionManager) + public function __construct(Security $security) { - $this->decisionManager = $decisionManager; + $this->security = $security; } protected function voteOnAttribute($attribute, $subject, TokenInterface $token) @@ -232,7 +232,7 @@ with ``ROLE_SUPER_ADMIN``:: // ... // ROLE_SUPER_ADMIN can do anything! The power! - if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) { + if ($this->securit->isGranted('ROLE_SUPER_ADMIN')) { return true; } @@ -241,19 +241,9 @@ with ``ROLE_SUPER_ADMIN``:: } If you're using the :ref:`default services.yaml configuration `, -you're done! Symfony will automatically pass the ``security.access.decision_manager`` +you're done! Symfony will automatically pass the ``security.helper`` service when instantiating your voter (thanks to autowiring). -Calling ``decide()`` on the ``AccessDecisionManager`` is essentially the same as -calling ``isGranted()`` from a controller or other places -(it's just a little lower-level, which is necessary for a voter). - -.. note:: - - If you need to check access in any non-voter service, use the ``security.authorization_checker`` - service (i.e. type-hint ``Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface``) - instead of the ``security.access.decision_manager`` service shown here. - .. _security-voters-change-strategy: Changing the Access Decision Strategy From 0c8d7c06a2803863c618e66e9fcc196b002019f6 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sat, 13 Oct 2018 13:47:52 -0700 Subject: [PATCH 2/5] Many changes thanks for GREAT feedback from various people --- controller/error_pages.rst | 6 +- doctrine/registration_form.rst | 8 +- security.rst | 96 +++++++++++++-------- security/auth_providers.rst | 20 ++++- security/custom_authentication_provider.rst | 8 +- security/expressions.rst | 28 ------ security/firewall_restriction.rst | 6 +- security/force_https.rst | 19 +++- security/guard_authentication.rst | 20 ++--- security/impersonating_user.rst | 4 +- security/named_encoders.rst | 3 + security/securing_services.rst | 4 +- security/user_provider.rst | 8 +- security/voters.rst | 2 +- 14 files changed, 128 insertions(+), 104 deletions(-) diff --git a/controller/error_pages.rst b/controller/error_pages.rst index 66e46d16bc2..5564fe9be88 100644 --- a/controller/error_pages.rst +++ b/controller/error_pages.rst @@ -60,7 +60,7 @@ logic to determine the template filename: a generic template for the given format (like ``error.json.twig`` or ``error.xml.twig``); -#. If none of the previous template exist, fall back to the generic HTML template +#. If none of the previous templates exist, fall back to the generic HTML template (``error.html.twig``). .. _overriding-or-adding-templates: @@ -69,7 +69,7 @@ To override these templates, rely on the standard Symfony method for :ref:`overriding templates that live inside a bundle ` and put them in the ``templates/bundles/TwigBundle/Exception/`` directory. -A typical project that returns HTML and JSON pages, might look like this: +A typical project that returns HTML and JSON pages might look like this: .. code-block:: text @@ -126,7 +126,7 @@ Security & 404 Pages -------------------- Due to the order of how routing and security are loaded, security information will -*not* be available on your 404 pages. This means that it will appear as if you're +*not* be available on your 404 pages. This means that it will appear as if your user is logged out on the 404 page (it will work while testing, but not on production). .. _testing-error-pages: diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index 054445acce7..009dcd75a20 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -23,12 +23,12 @@ Your ``User`` entity will probably at least have the following fields: ``username`` This will be used for logging in, unless you instead want your user to - :ref:`login via email ` (in that case, this + :ref:`log in via email ` (in that case, this field is unnecessary). ``email`` A nice piece of information to collect. You can also allow users to - :ref:`login via email `. + :ref:`log in via email `. ``password`` The encoded password. @@ -418,9 +418,7 @@ us to add validation, even though there is no ``termsAccepted`` property on ``Us Manually Authenticating after Success ------------------------------------- -If you're using Guard authentication, you can :ref:`automatically authenticate` +If you're using Guard authentication, you can :ref:`automatically authenticate ` after registration is successful. -.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form -.. _`FOSUserBundle`: https://github.com/FriendsOfSymfony/FOSUserBundle .. _`bcrypt`: https://en.wikipedia.org/wiki/Bcrypt diff --git a/security.rst b/security.rst index f1a32eba9ba..9c7ebd318b9 100644 --- a/security.rst +++ b/security.rst @@ -10,7 +10,7 @@ Security Do you prefer video tutorials? Check out the `Symfony Security screencast series`_. Symfony's security system is incredibly powerful, but it can also be confusing -to set up. But don't worry! In this article, you'll learn how to set up your app's +to set up. Don't worry! In this article, you'll learn how to set up your app's security system step-by-step: #. :ref:`Installing security support `; @@ -41,12 +41,12 @@ install the security feature before using it: .. _initial-security-yaml-setup-authentication: .. _create-user-class: -2) Create your User Class -------------------------- +2a) Create your User Class +-------------------------- No matter *how* you will authenticate (e.g. login form or API tokens) or *where* -your user data will be stored (database, SSO), the next step is always the same: -create a "User" class. The easiest way is to use `MakerBundle`_. +your user data will be stored (database, single sign-on), the next step is always the same: +create a "User" class. The easiest way is to use the `MakerBundle`_. Let's assume that you want to store your user data in the database with Doctrine: @@ -122,7 +122,8 @@ command will pre-configure this for you: # ... encoders: - Symfony\Component\Security\Core\User\User: + # use your user class name here + App\Entity\User: # bcrypt or argon21 are recommended # argon21 is more secure, but requires PHP 7.2 or the Sodium extension algorithm: bcrypt @@ -141,7 +142,7 @@ command will pre-configure this for you: - @@ -156,7 +157,7 @@ command will pre-configure this for you: // ... 'encoders' => array( - 'Symfony\Component\Security\Core\User\User' => array( + 'App\Entity\User' => array( 'algorithm' => 'bcrypt', 'cost' => 12, ) @@ -210,7 +211,7 @@ Use this service to encode the passwords: } } -Of, you can manually encode a password by running: +You can manually encode a password by running: .. code-block:: terminal @@ -220,8 +221,8 @@ Of, you can manually encode a password by running: .. _security-firewalls: .. _firewalls-authentication: -3) Authentication & Firewalls ------------------------------ +3a) Authentication & Firewalls +------------------------------ The security system is configured in ``config/packages/security.yaml``. The *most* important section is ``firewalls``: @@ -290,7 +291,8 @@ Nope, thanks to the ``anonymous`` key, this firewall *is* accessible anonymously In fact, if you go to the homepage right now, you *will* have access and you'll see that you're "authenticated" as ``anon.``. Don't be fooled by the "Yes" next to -Authenticated, you're just an anonymous user: +Authenticated. The firewall verified that it does not know your identity, and so, +you are anonymous: .. image:: /_images/security/anonymous_wdt.png :align: center @@ -299,7 +301,7 @@ You'll learn later how to deny access to certain URLs or controllers. .. note:: - If you do not see toolbar, install the :doc:`profiler ` with: + If you do not see the toolbar, install the :doc:`profiler ` with: .. code-block:: terminal @@ -348,7 +350,7 @@ For the most detailed description of authenticators and how they work, see 4) Denying Access, Roles and other Authorization ------------------------------------------------ -Users can now login to your app using your login form. Great! Now, you need to learn +Users can now log in to your app using your login form. Great! Now, you need to learn how to deny access and work with the User object. This is called **authorization**, and its job is to decide if a user can access some resource (a URL, a model object, a method call, ...). @@ -364,7 +366,7 @@ Roles ~~~~~ When a user logs in, Symfony calls the ``getRoles()`` method on your ``User`` -object to determime which roles this user has. In the ``User`` class that we +object to determine which roles this user has. In the ``User`` class that we generated earlier, the roles are an array that's stored in the database, and every user is *always* given at least one role: ``ROLE_USER``:: @@ -388,16 +390,14 @@ every user is *always* given at least one role: ``ROLE_USER``:: This is a nice default, but you can do *whatever* you want to determine which roles a user should have. Here are a few guidelines: -* Every role **must start with** ``ROLE_`` (otherwise, things won't as expected) +* Every role **must start with** ``ROLE_`` (otherwise, things won't work as expected) * Other than the above rule, a role is just a string and you can invent what you - need (e.g. ``ROLE_PRODUCT_ADMIN``) - -* Every User **must** have at least **one** role - a common convention is to give - *every* user ``ROLE_USER``. + need (e.g. ``ROLE_PRODUCT_ADMIN``). +You'll use these roles next to grant access to specific sections of your site. You can also use a :ref:`role hierarchy ` where having -some roles automatically gives you other roles. +some roles automatically give you other roles. .. _security-role-authorization: @@ -531,7 +531,7 @@ Prepending the path with ``^`` means that only URLs *beginning* with the pattern are matched. For example, a path of simply ``/admin`` (without the ``^``) would match ``/admin/foo`` but would also match URLs like ``/foo/admin``. -Each ``access_control`` can also match on IP address, host name and HTTP methods. +Each ``access_control`` can also match on IP address, hostname and HTTP methods. It can also be used to redirect a user to the ``https`` version of a URL pattern. See :doc:`/security/access_control`. @@ -550,7 +550,7 @@ You can easily deny access from inside a controller:: $this->denyAccessUnlessGranted('ROLE_ADMIN'); // or add an optional message - seen by developers - $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'Unable to access this page!'); + $this->denyAccessUnlessGranted('ROLE_ADMIN', null, 'User tried to access a page without having ROLE_ADMIN'); } That's it! If access is not granted, a special @@ -558,7 +558,7 @@ That's it! If access is not granted, a special is thrown and no more code in your controller is executed. Then, one of two things will happen: -1) If the user isn't logged in yet, they will be asked to login (e.g. redirected +1) If the user isn't logged in yet, they will be asked to log in (e.g. redirected to the login page). 2) If the user *is* logged in, but does *not* have the ``ROLE_ADMIN`` role, they'll @@ -637,7 +637,7 @@ You can use ``IS_AUTHENTICATED_FULLY`` anywhere roles are used: like ``access_co or in Twig. ``IS_AUTHENTICATED_FULLY`` isn't a role, but it kind of acts like one, and every -user that has logged in will have this. ACtually, there are 3 special attributes +user that has logged in will have this. Actually, there are 3 special attributes like this: * ``IS_AUTHENTICATED_REMEMBERED``: *All* logged in users have this, even @@ -671,8 +671,8 @@ If you still prefer to use traditional ACLs, refer to the `Symfony ACL bundle`_. .. _retrieving-the-user-object: -5) Fetching the User Object ---------------------------- +5a) Fetching the User Object +---------------------------- After authentication, the ``User`` object of the current user can be accessed via the ``getUser()`` shortcut:: @@ -683,6 +683,8 @@ via the ``getUser()`` shortcut:: $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); // returns your User object, or null if the user is not authenticated + // use inline documentation to tell your editor your exact User class + /** @var \App\Entity\User $user */ $user = $this->getUser(); // Call whatever methods you've added to your User class @@ -690,7 +692,7 @@ via the ``getUser()`` shortcut:: return new Response('Well hi there '.$user->getFirstName()); } -5B) Fetching the User from a Service +5b) Fetching the User from a Service ------------------------------------ If you need to get the logged in user from a service, use the @@ -707,6 +709,8 @@ If you need to get the logged in user from a service, use the public function __construct(Security $security) { + // Avoid calling getUser() in the constructor: auth may not + // be complete yet. Instead, store the entire Security object. $this->security = $security; } @@ -734,7 +738,7 @@ key: Logging Out ----------- -To add logout, activate the ``logout`` config parameter under your firewall: +To enable logging out, activate the ``logout`` config parameter under your firewall: .. configuration-block:: @@ -808,11 +812,12 @@ Next, you'll need to create a route for this URL (but not a controller): class SecurityController extends AbstractController { /** - * @Route("/login", name="app_logout") + * @Route("/logout", name="app_logout") */ public function logout() { - // controller should be blank - will never be executed + // controller can be blank: it will never be executed! + throw new \Exception('Don\'t forget to activate logout in security.yaml'); } } @@ -905,9 +910,18 @@ Users with the ``ROLE_ADMIN`` role will also have the ``ROLE_USER`` role. And users with ``ROLE_SUPER_ADMIN``, will automatically have ``ROLE_ADMIN``, ``ROLE_ALLOWED_TO_SWITCH`` and ``ROLE_USER`` (inherited from ``ROLE_ADMIN``). +For role hierarchy to work, do not try to call ``$user->getRoles()`` manually:: + + // BAD - $user->getRoles() will not know about the role hierarchy + $hasAccess = in_array('ROLE_ADMIN', $user->getRoles()); + + // GOOD - use of the normal security methods + $hasAccess = $this->isGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_ADMIN'); + .. note:: - The ``role_hierarchy`` values iare static - you can't, for example, store the + The ``role_hierarchy`` values are static - you can't, for example, store the role hierarchy in a database. If you need that, create a custom :doc:`security voter ` that looks for the user roles in the database. @@ -923,7 +937,9 @@ Frequently Asked Questions **Can I have Multiple Firewalls?** Yes! But it's usually not necessary. Each firewall is like a separate security system. And so, unless you have *very* different authentication needs, one - firewall usually works well. + firewall usually works well. With :doc:`Guard authentication `, + you can create various, diverse ways of allowing authentication (e.g. form login, + API key authentication and LDAP) all under the same firewall. **Can I Share Authentication Between Firewalls?** Yes, but only with some configuration. If you're using multiple firewalls and @@ -942,11 +958,15 @@ Frequently Asked Questions **My Authentication Doesn't Seem to Work: No Errors, but I'm Never Logged In** Sometimes authentication may be successful, but after redirecting, you're logged out immediately due to a problem loading the ``User`` from the session. - To see if this is the issue, temporarily enable :ref:`intercept_redirects`. - Then, when you login, instead of being redirected, you'll be stopped. Check - the web debug toolbar on that page to see if you're logged in. If you *are*, - but are no longer logged in after redirecting, then there is a problem loading - your User from the session. See :ref:`user_session_refresh`. + To see if this is an issue, check your log file (``var/log/dev.log``) for + the log message: + + > Cannot refresh token because user has changed. + + If you see this, there are two possible causes. First, there may be a problem + loading your User from the session. See :ref:`user_session_refresh`. Second, + if certain user information was changed in the database since the last page + refresh, Symfony will purposely log out the user for security reasons. Learn More ---------- diff --git a/security/auth_providers.rst b/security/auth_providers.rst index 1478657ec1b..102a52b3dd3 100644 --- a/security/auth_providers.rst +++ b/security/auth_providers.rst @@ -29,6 +29,10 @@ use-case matches one of these exactly, they're a great option: HTTP Basic Authentication ------------------------- +`HTTP Basic authentication`_ asks credentials (username and password) using a dialog +in the browser. The credentials are sent without any hashing or encryption, so +it's recommended to use it with HTTPS. + To support HTTP Basic authentication, add the ``http_basic`` key to your firewall: .. configuration-block:: @@ -72,7 +76,7 @@ To support HTTP Basic authentication, add the ``http_basic`` key to your firewal 'firewalls' => array( 'main' => array( - 'http_basic' => array( + 'http_basic' => array( 'realm' => 'Secured Area', ), ), @@ -82,12 +86,16 @@ To support HTTP Basic authentication, add the ``http_basic`` key to your firewal That's it! Symfony will now be listening for any HTTP basic authentication data. To load user information, it will use your configured :doc:`user provider `. +Note: you cannot use the :ref:`log out ` with ``http_basic``. +Even if you log out, your browser "remembers" your credentials and will send them +on every request. + .. _security-x509: X.509 Client Certificate Authentication --------------------------------------- -When using client certificates, your webserver is doing all the authentication +When using client certificates, your web server is doing all the authentication process itself. With Apache, for example, you would use the ``SSLVerifyClient Require`` directive. @@ -121,6 +129,7 @@ Enable the x509 authentication for a particular firewall in the security configu + @@ -134,7 +143,8 @@ Enable the x509 authentication for a particular firewall in the security configu 'firewalls' => array( 'main' => array( - 'x509' => array( + // ... + 'x509' => array( 'provider' => 'your_user_provider', ), ), @@ -165,7 +175,7 @@ in the x509 firewall configuration respectively. REMOTE_USER Based Authentication -------------------------------- -A lot of authentication modules, like ``auth_kerb`` for Apache provide the username +A lot of authentication modules, like ``auth_kerb`` for Apache, provide the username using the ``REMOTE_USER`` environment variable. This variable can be trusted by the application since the authentication happened before the request reached it. @@ -220,3 +230,5 @@ key in the ``remote_user`` firewall configuration. Just like for X509 authentication, you will need to configure a "user provider". See :ref:`the previous note ` for more information. + +.. _`HTTP Basic authentication`: https://en.wikipedia.org/wiki/Basic_access_authentication diff --git a/security/custom_authentication_provider.rst b/security/custom_authentication_provider.rst index 599b528b02d..bae38cee504 100644 --- a/security/custom_authentication_provider.rst +++ b/security/custom_authentication_provider.rst @@ -12,9 +12,11 @@ How to Create a custom Authentication Provider keep reading unless you want to learn the lowest level details of authentication. -If you have read the article on :doc:`/security`, you understand the -distinction Symfony makes between authentication and authorization in the -implementation of security. This article discusses the core classes involved +Symfony provides support for the most +:doc:`common authentication mechanisms `. However, your +app may need to integrated with some proprietary single-sing-on system or some +legacy authentication mechanism. In those cases you could create a custom +authentication provider. This article discusses the core classes involved in the authentication process, and how to implement a custom authentication provider. Because authentication and authorization are separate concepts, this extension will be user-provider agnostic, and will function with your diff --git a/security/expressions.rst b/security/expressions.rst index d445bc6218b..657ffc1dca4 100644 --- a/security/expressions.rst +++ b/security/expressions.rst @@ -92,34 +92,6 @@ Additionally, you have access to a number of functions inside the expression: true if the user has actually logged in during this session (i.e. is full-fledged). -TODO - moved from main security chapter ---- - -.. _security-template-expression: - -You can also use expressions inside your templates: - -.. configuration-block:: - - .. code-block:: html+jinja - - {% if is_granted(expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - )) %} - Delete - {% endif %} - - .. code-block:: html+php - - isGranted(new Expression( - '"ROLE_ADMIN" in roles or (not is_anonymous() and user.isSuperAdmin())' - ))): ?> - Delete - - -For more details on expressions and security, see :doc:`/security/expressions`. - - Learn more ---------- diff --git a/security/firewall_restriction.rst b/security/firewall_restriction.rst index d029f5bfd7e..bb83235ce24 100644 --- a/security/firewall_restriction.rst +++ b/security/firewall_restriction.rst @@ -1,11 +1,11 @@ .. index:: single: Security; Restrict Security Firewalls to a Request -How to Restrict Firewalls by Pattern, Host or HTTP Method -========================================================= +How to Restrict Firewalls to a Request +====================================== When using the Security component, you can create firewalls that match certain request options. -In most cases, matching against the URL is sufficient, but in special cases you can further +In most cases, matching against the URL is sufficient, but in special cases, you can further restrict the initialization of a firewall against other options of the request. .. note:: diff --git a/security/force_https.rst b/security/force_https.rst index 50ab5b3ed8b..cee83d6dab0 100644 --- a/security/force_https.rst +++ b/security/force_https.rst @@ -11,8 +11,8 @@ How to Force HTTPS or HTTP for different URLs You can force areas of your site to use the HTTPS protocol in the security config. This is done through the ``access_control`` rules using the ``requires_channel`` -option. For example, suppose you want to force all URLs starting with ``/secure`` -to use HTTPS as well as the login form: +option. To enforce HTTPS on all URLs, add the ``requires_channel`` config to every +access control: .. configuration-block:: @@ -25,6 +25,8 @@ to use HTTPS as well as the login form: access_control: - { path: ^/secure, roles: ROLE_ADMIN, requires_channel: https } - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } + # catch all other URLs + - { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } .. code-block:: xml @@ -44,6 +46,10 @@ to use HTTPS as well as the login form: role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https" /> + @@ -64,9 +70,18 @@ to use HTTPS as well as the login form: 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https', ), + array( + 'path' => '^/', + 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', + 'requires_channel' => 'https', + ), ), )); +To make life easier while developing, you can also use an environment variable, +like ``requires_channel: '%env(SECURE_SCHEME)%'``. In your ``.env`` file, set +``SECURE_SCHEME`` to ``http`` locally, but ``https`` on production. + See :doc:`/security/access_control` for more details about ``access_control`` in general. diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst index 416eda65c8a..6893aac9c5e 100644 --- a/security/guard_authentication.rst +++ b/security/guard_authentication.rst @@ -8,16 +8,16 @@ Whether you need to build a traditional login form, an API token authentication or you need to integrate with some proprietary single-sign-on system, the Guard component can make it easy... and fun! -Guard authentication can easily be used to +Guard authentication can be used to: * :doc:`Build a Login Form `, * Create an API token authentication system (done on this page!) * `Social Authentication`_ (or use `HWIOAuthBundle`_ for a robust, but non-Guard solution) -In this example, we'll build an API token authentication system so we can learn -more about Guard in detail. +or anything else you dream up. In this example, we'll build an API token authentication +system so we can learn more about Guard in detail. -Step 0) Prepare your User Class +Step 1) Prepare your User Class ------------------------------- Suppose you want to build an API where your clients will send an ``X-AUTH-TOKEN`` header @@ -52,7 +52,7 @@ Don't forget to generate and execute the migration: $ php bin/console make:migration $ php bin/console doctrine:migrations:migrate -Step 1) Create the Authenticator Class +Step 2) Create the Authenticator Class -------------------------------------- To create a custom authentication system, just create a class and make it implement @@ -167,7 +167,7 @@ This requires you to implement several methods:: Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods`. -Step 2) Configure the Authenticator +Step 3) Configure the Authenticator ----------------------------------- To finish this, make sure your authenticator is registered as a service. If you're @@ -406,17 +406,17 @@ completes registration. To do that, use your authenticator and a service called class RegistrationController extends AbstractController { - public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $reques) + public function register(LoginFormAuthenticator $authenticator, GuardAuthenticatorHandler $guardHandler, Request $request) { // ... - // after validating the user and saving them to the database + // after validating the user and saving them to the database // authenticate the user and use onAuthenticationSuccess on the authenticator return $guardHandler->authenticateUserAndHandleSuccess( $user, // the User object you just created $request, $authenticator, // authenticator whose onAuthenticationSuccess you want to use - 'main' // the name if your firewall in security.yaml + 'main' // the name of your firewall in security.yaml ); } } @@ -494,7 +494,7 @@ Frequently Asked Questions -------------------------- **Can I have Multiple Authenticators?** - Yes! But when you do, you'll need choose just *one* authenticator to be your + Yes! But when you do, you'll need to choose just *one* authenticator to be your "entry_point". This means you'll need to choose *which* authenticator's ``start()`` method should be called when an anonymous user tries to access a protected resource. For more details, see :doc:`/security/multiple_guard_authenticators`. diff --git a/security/impersonating_user.rst b/security/impersonating_user.rst index b64f2d220a8..ec56067ad09 100644 --- a/security/impersonating_user.rst +++ b/security/impersonating_user.rst @@ -79,7 +79,7 @@ To switch back to the original user, use the special ``_exit`` username: http://example.com/somewhere?_switch_user=_exit This feature is only available to users with a special role called ``ROLE_ALLOWED_TO_SWITCH``. -Using :ref:`role_hierarchy` is a great way to give this +Using :ref:`role_hierarchy ` is a great way to give this role to the users that need it. Knowing When Impersonation Is Active @@ -98,7 +98,7 @@ to show a link to exit impersonation: Finding the Original User ------------------------- -In some cases you may need to get the object that represents the impersonator +In some cases, you may need to get the object that represents the impersonator user rather than the impersonated user. Use the following snippet to iterate over the user's roles until you find one that is a ``SwitchUserRole`` object:: diff --git a/security/named_encoders.rst b/security/named_encoders.rst index 7826fe865f9..c7fba3e7293 100644 --- a/security/named_encoders.rst +++ b/security/named_encoders.rst @@ -17,6 +17,7 @@ to apply to all instances of a specific class: encoders: App\Entity\User: algorithm: bcrypt + cost: 12 .. code-block:: xml @@ -32,6 +33,7 @@ to apply to all instances of a specific class: @@ -46,6 +48,7 @@ to apply to all instances of a specific class: 'encoders' => array( User::class => array( 'algorithm' => 'bcrypt', + 'cost' => 12, ), ), )); diff --git a/security/securing_services.rst b/security/securing_services.rst index 14341166150..67b37dd792e 100644 --- a/security/securing_services.rst +++ b/security/securing_services.rst @@ -8,9 +8,9 @@ How to Secure any Service or Method in your Application In the security article, you learned how to :ref:`secure a controller ` via a shortcut method. -But, you can check access *anywhere* in your code by injection the ``Security`` +But, you can check access *anywhere* in your code by injecting the ``Security`` service. For example, suppose you have a ``SalesReportManager`` service and you -want to include extra details onlt for users that have some ``ROLE_SALES_ADMIN`` role: +want to include extra details only for users that have a ``ROLE_SALES_ADMIN`` role: .. code-block:: diff diff --git a/security/user_provider.rst b/security/user_provider.rst index 98192c8a025..59e873b1221 100644 --- a/security/user_provider.rst +++ b/security/user_provider.rst @@ -8,7 +8,8 @@ that has two jobs: At the beginning of each request (unless your firewall is ``stateless``), Symfony loads the ``User`` object from the session. To make sure it's not out-of-date, the user provider "refreshes it". The Doctrine user provider, for example, - queries the database for fresh data. + queries the database for fresh data. Symfony then checks to see if the user + has "changed" and de-authenticates the user if they have (see :ref:`user_session_refresh`). **Load the User for some Feature** Some features, like ``switch_user``, ``remember_me`` and many of the built-in @@ -36,6 +37,7 @@ User providers are configured in ``config/packages/security.yaml`` under the # config/packages/security.yaml security: + # ... providers: # this becomes the internal name of the provider # not usually important, but can be used to specify which @@ -85,6 +87,8 @@ generate a nice skeleton to get you started:: * this method. * * @return UserInterface + * + * @throws UsernameNotFoundException if the user is not found */ public function loadUserByUsername($username) { @@ -114,8 +118,6 @@ generate a nice skeleton to get you started:: throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user))); } - /* @var User $user */ - // Return a User object after making sure its data is "fresh". // Or throw a UsernameNotFoundException if the user no longer exists. throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__); diff --git a/security/voters.rst b/security/voters.rst index 5fcbcc0a7c4..5ed6d1583ec 100644 --- a/security/voters.rst +++ b/security/voters.rst @@ -232,7 +232,7 @@ with ``ROLE_SUPER_ADMIN``:: // ... // ROLE_SUPER_ADMIN can do anything! The power! - if ($this->securit->isGranted('ROLE_SUPER_ADMIN')) { + if ($this->security->isGranted('ROLE_SUPER_ADMIN')) { return true; } From 3e63a6ce9687559810e780b2842a4e0590db8b48 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 17 Oct 2018 22:36:16 -0400 Subject: [PATCH 3/5] Tweaks based on feedback --- security/user_checkers.rst | 6 ++++++ security/user_provider.rst | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/security/user_checkers.rst b/security/user_checkers.rst index d7da97d9ff4..8e13b8c767a 100644 --- a/security/user_checkers.rst +++ b/security/user_checkers.rst @@ -25,6 +25,7 @@ are not met, an exception should be thrown which extends the use App\Exception\AccountDeletedException; use App\Security\User as AppUser; use Symfony\Component\Security\Core\Exception\AccountExpiredException; + use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; @@ -39,6 +40,11 @@ are not met, an exception should be thrown which extends the // user is deleted, show a generic Account Not Found message. if ($user->isDeleted()) { throw new AccountDeletedException('...'); + + // or to customize the message shown + throw new CustomUserMessageAuthenticationException( + 'Your account was deleted. Sorry about that!' + ); } } diff --git a/security/user_provider.rst b/security/user_provider.rst index 59e873b1221..7b5ce440b33 100644 --- a/security/user_provider.rst +++ b/security/user_provider.rst @@ -49,8 +49,8 @@ User providers are configured in ``config/packages/security.yaml`` under the memory: # custom options for that provider users: - user: { password: userpass, roles: [ 'ROLE_USER' ] } - admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] } + user: { password: '%env(USER_PASSWORD)%', roles: [ 'ROLE_USER' ] } + admin: { password: '%env(ADMIN_PASSWORD)%', roles: [ 'ROLE_ADMIN' ] } a_chain_provider: chain: From 7d01771d9d6e2f2d705d043e7fb551f2a167d0a5 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 17 Oct 2018 22:41:33 -0400 Subject: [PATCH 4/5] Fixing missing link --- doctrine/registration_form.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doctrine/registration_form.rst b/doctrine/registration_form.rst index 009dcd75a20..229040acac2 100644 --- a/doctrine/registration_form.rst +++ b/doctrine/registration_form.rst @@ -421,4 +421,5 @@ Manually Authenticating after Success If you're using Guard authentication, you can :ref:`automatically authenticate ` after registration is successful. +.. _`CVE-2013-5750`: https://symfony.com/blog/cve-2013-5750-security-issue-in-fosuserbundle-login-form .. _`bcrypt`: https://en.wikipedia.org/wiki/Bcrypt From ed3dd653dd8a18e78de7f3db38690b57479da2a4 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 17 Oct 2018 23:00:12 -0400 Subject: [PATCH 5/5] removing old reference --- reference/twig_reference.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/twig_reference.rst b/reference/twig_reference.rst index ebaf439f15b..bd2186e2f99 100644 --- a/reference/twig_reference.rst +++ b/reference/twig_reference.rst @@ -430,7 +430,7 @@ expression ~~~~~~~~~~ Creates an :class:`Symfony\\Component\\ExpressionLanguage\\Expression` in -Twig. See ":ref:`Template Expressions `". +Twig. .. _reference-twig-filters: