diff --git a/security.rst b/security.rst index 584db4b78b5..15a56b5b245 100644 --- a/security.rst +++ b/security.rst @@ -37,6 +37,49 @@ install the security feature before using it: $ composer require symfony/security-bundle + +.. tip:: + + A :doc:`new experimental Security ` + was introduced in Symfony 5.1, which will eventually replace security in + Symfony 6.0. This system is almost fully backwards compatible with the + current Symfony security, add this line to your security configuration to start + using it: + + .. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + enable_authenticator_manager: true + # ... + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + // ... + ]); + .. _initial-security-yml-setup-authentication: .. _initial-security-yaml-setup-authentication: .. _create-user-class: @@ -1121,6 +1164,7 @@ Authentication (Identifying/Logging in the User) .. toctree:: :maxdepth: 1 + security/experimental_authenticators security/form_login_setup security/reset_password security/json_login_setup diff --git a/security/experimental_authenticators.rst b/security/experimental_authenticators.rst new file mode 100644 index 00000000000..4299a452dcf --- /dev/null +++ b/security/experimental_authenticators.rst @@ -0,0 +1,499 @@ +Using the new Authenticator-based Security +========================================== + +.. versionadded:: 5.1 + + Authenticator-based security was introduced as an + :doc:`experimental feature ` in + Symfony 5.1. + +In Symfony 5.1, a new authentication system was introduced. This system +changes the internals of Symfony Security, to make it more extensible +and more understandable. + +Enabling the System +------------------- + +The authenticator-based system can be enabled using the +``enable_authenticator_manager`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + enable_authenticator_manager: true + # ... + + .. code-block:: xml + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + // ... + ]); + +The new system is backwards compatible with the current authentication +system, with some exceptions that will be explained in this article: + +* :ref:`Anonymous users no longer exist ` +* :ref:`Configuring the authentication entry point is required when more than one authenticator is used ` +* :ref:`The authentication providers are refactored into Authenticators ` + +.. _authenticators-removed-anonymous: + +Adding Support for Unsecured Access (i.e. Anonymous Users) +---------------------------------------------------------- + +In Symfony, visitors that haven't yet logged in to your website were called +:ref:`anonymous users `. The new system no longer +has anonymous authentication. Instead, these sessions are now treated as +unauthenticated (i.e. there is no security token). When using +``isGranted()``, the result will always be ``false`` (i.e. denied) as this +session is handled as a user without any privileges. + +In the ``access_control`` configuration, you can use the new +``PUBLIC_ACCESS`` security attribute to whitelist some routes for +unauthenticated access (e.g. the login page): + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + enable_authenticator_manager: true + + # ... + access_control: + # allow unauthenticated users to access the login form + - { path: ^/admin/login, roles: PUBLIC_ACCESS } + + # but require authentication for all other admin routes + - { path: ^/admin, roles: ROLE_ADMIN } + + .. code-block:: xml + + + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Component\Security\Http\Firewall\AccessListener; + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + + // ... + 'access_control' => [ + // allow unauthenticated users to access the login form + ['path' => '^/admin/login', 'roles' => AccessListener::PUBLIC_ACCESS], + + // but require authentication for all other admin routes + ['path' => '^/admin', 'roles' => 'ROLE_ADMIN'], + ], + ]); + +.. _authenticators-required-entry-point: + +Configuring the Authentication Entry Point +------------------------------------------ + +Sometimes, one firewall has multiple ways to authenticate (e.g. both a form +login and an API token authentication). In these cases, it is now required +to configure the *authentication entry point*. The entry point is used to +generate a response when the user is not yet authenticated but tries to access +a page that requires authentication. This can be used for instance to redirect +the user to the login page. + +You can configure this using the ``entry_point`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + enable_authenticator_manager: true + + # ... + firewalls: + main: + # allow authentication using a form or HTTP basic + form_login: ~ + http_basic: ~ + + # configure the form authentication as the entry point for unauthenticated users + entry_point: form_login + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // config/packages/security.php + use Symfony\Component\Security\Http\Firewall\AccessListener; + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + + // ... + 'firewalls' => [ + 'main' => [ + // allow authentication using a form or HTTP basic + 'form_login' => null, + 'http_basic' => null, + + // configure the form authentication as the entry point for unauthenticated users + 'entry_point' => 'form_login' + ], + ], + ]); + +.. note:: + + You can also create your own authentication entry point by creating a + class that implements + :class:`Symfony\\Component\\Security\\Http\\EntryPoint\\AuthenticationEntryPointInterface`. + You can then set ``entry_point`` to the service id (e.g. + ``entry_point: App\Security\CustomEntryPoint``) + +.. _authenticators-removed-authentication-providers: + +Creating a Custom Authenticator +------------------------------- + +Security traditionally could be extended by writing +:doc:`custom authentication providers `. +The authenticator-based system dropped support for these providers and +introduced a new authenticator interface as a base for custom +authentication methods. + +.. tip:: + + :doc:`Guard authenticators ` are still + supported in the authenticator-based system. It is however recommended + to also update these when you're refactoring your application to the + new system. The new authenticator interface has many similarities with the + guard authenticator interface, making the rewrite easier. + +Authenticators should implement the +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AuthenticatorInterface`. +You can also extend +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractAuthenticator`, +which has a default implementation for the ``createAuthenticatedToken()`` +method that fits most use-cases:: + + // src/Security/ApiKeyAuthenticator.php + namespace App\Security; + + use App\Entity\User; + use Doctrine\ORM\EntityManagerInterface; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\Response; + 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\UsernameNotFoundException; + use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; + use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; + use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; + + class ApiKeyAuthenticator extends AbstractAuthenticator + { + private $entityManager; + + public function __construct(EntityManagerInterface $entityManager) + { + $this->entityManager = $entityManager; + } + + /** + * Called on every request to decide if this authenticator should be + * used for the request. Returning `false` will cause this authenticator + * to be skipped. + */ + public function supports(Request $request): ?bool + { + return $request->headers->has('X-AUTH-TOKEN'); + } + + public function authenticate(Request $request): PassportInterface + { + $apiToken = $request->headers->get('X-AUTH-TOKEN'); + if (null === $apiToken) { + // The token header was empty, authentication fails with HTTP Status + // Code 401 "Unauthorized" + throw new CustomUserMessageAuthenticationException('No API token provided'); + } + + $user = $this->entityManager->getRepository(User::class) + ->findOneBy(['apiToken' => $apiToken]) + ; + if (null === $user) { + throw new UsernameNotFoundException(); + } + + return new SelfValidatingPassport($user); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + // on success, let the request continue + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response + { + $data = [ + // you may want to customize or obfuscate the message first + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) + + // or to translate this message + // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) + ]; + + return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); + } + } + +The authenticator can be enabled using the ``custom_authenticators`` setting: + +.. configuration-block:: + + .. code-block:: yaml + + # config/packages/security.yaml + security: + enable_authenticator_manager: true + + # ... + firewalls: + main: + custom_authenticators: + - App\Security\ApiKeyAuthenticator + + # don't forget to also configure the entry_point if the + # authenticator implements AuthenticatorEntryPointInterface + # entry_point: App\Security\CustomFormLoginAuthenticator + + .. code-block:: xml + + + + + + + + + + + + App\Security\ApiKeyAuthenticator + + + + + .. code-block:: php + + // config/packages/security.php + use App\Security\ApiKeyAuthenticator; + use Symfony\Component\Security\Http\Firewall\AccessListener; + + $container->loadFromExtension('security', [ + 'enable_authenticator_manager' => true, + + // ... + 'firewalls' => [ + 'main' => [ + 'custom_authenticators' => [ + ApiKeyAuthenticator::class, + ], + + // don't forget to also configure the entry_point if the + // authenticator implements AuthenticatorEntryPointInterface + // 'entry_point' => [App\Security\CustomFormLoginAuthenticator::class], + ], + ], + ]); + +The ``authenticate()`` method is the most important method of the +authenticator. Its job is to extract credentials (e.g. username & +password, or API tokens) from the ``Request`` object and transform these +into a security +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport`. + +.. tip:: + + If you want to customize the login form, you can also extend from the + :class:`Symfony\\Component\\Security\\Http\\Authenticator\\AbstractLoginFormAuthenticator` + class instead. + +Security Passports +~~~~~~~~~~~~~~~~~~ + +A passport is an object that contains the user that will be authenticated as +well as other pieces of information, like whether a password should be checked +or if "remember me" functionality should be enabled. + +The default +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Passport`. +requires a user object and credentials. The following credential classes +are supported by default: + + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\PasswordCredentials` + This requires a plaintext ``$password``, which is validated using the + :ref:`password encoder configured for the user `. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Credentials\\CustomCredentials` + Allows a custom closure to check credentials:: + + // ... + return new Passport($user, new CustomCredentials( + // If this function returns anything else than `true`, the credentials + // are marked as invalid. + // The $credentials parameter is equal to the next argument of this class + function ($credentials, UserInterface $user) { + return $user->getApiToken() === $credentials; + }, + + // The custom credentials + $apiToken + )); + +.. note:: + + If you don't need any credentials to be checked (e.g. a JWT token), you + can use the + :class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\SelfValidatingPassport`. + This class only requires a user and optionally `Passport Badges`_. + +Passport Badges +~~~~~~~~~~~~~~~ + +The ``Passport`` also optionally allows you to add *security badges*. +Badges attach more data to the passport (to extend security). By default, +the following badges are supported: + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\RememberMeBadge` + When this badge is added to the passport, the authenticator indicates + remember me is supported. Whether remember me is actually used depends + on special ``remember_me`` configuration. Read + :doc:`/security/remember_me` for more information. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PasswordUpgradeBadge` + This is used to automatically upgrade the password to a new hash upon + successful login. This badge requires the plaintext password and a + password upgrader (e.g. the user repository). See :doc:`/security/password_migration`. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\CsrfTokenBadge` + Automatically validates CSRF tokens for this authenticator during + authentication. The constructor requires a token ID (unique per form) + and CSRF token (unique per request). See :doc:`/security/csrf`. + +:class:`Symfony\\Component\\Security\\Http\\Authenticator\\Passport\\Badge\\PreAuthenticatedBadge` + Indicates that this user was pre-authenticated (i.e. before Symfony was + initiated). This skips the + :doc:`pre-authentication user checker `. + +For instance, if you want to add CSRF and password migration to your custom +authenticator, you would initialize the passport like this:: + + // ... + use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Badge\PasswordUpgradeBadge; + use Symfony\Component\Security\Http\Authenticator\Passport\Passport; + use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface; + + class LoginAuthenticator extends AbstractAuthenticator + { + public function authenticate(Request $request): PassportInterface + { + $password = $request->request->get('password'); + $username = $request->request->get('username'); + $csrfToken = $request->request->get('csrf_token'); + + // ... get the $user from the $username and validate no + // parameter is empty + + return new Passport($user, new PasswordCredentials($password), [ + // $this->userRepository must implement PasswordUpgraderInterface + new PasswordUpgradeBadge($password, $this->userRepository), + new CsrfTokenBadge('login', $csrfToken); + ]); + } + } diff --git a/security/guard_authentication.rst b/security/guard_authentication.rst index 71859870d11..d88bea0de20 100644 --- a/security/guard_authentication.rst +++ b/security/guard_authentication.rst @@ -14,6 +14,11 @@ Guard authentication can be used to: and many more. In this example, we'll build an API token authentication system, so we can learn more about Guard in detail. +.. tip:: + + A :doc:`new experimental authenticator-based system ` + was introduced in Symfony 5.1, which will eventually replace Guards in Symfony 6.0. + Step 1) Prepare your User Class -------------------------------