From 110e22b37734ace0c2a97858d00123f38aa53460 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Tue, 17 Dec 2013 06:13:23 -0600 Subject: [PATCH 1/2] [#2914][WIP] Additions to the new authentication articles The biggest difference is the extension of the API article to give more details about the user provider and to talk about how you might store authentication information in the session. --- cookbook/security/_supportsToken.rst.inc | 10 + cookbook/security/api_key_authentication.rst | 394 ++++++++++++++++-- .../custom_authentication_provider.rst | 9 + .../custom_password_authenticator.rst | 95 +++-- 4 files changed, 446 insertions(+), 62 deletions(-) create mode 100644 cookbook/security/_supportsToken.rst.inc diff --git a/cookbook/security/_supportsToken.rst.inc b/cookbook/security/_supportsToken.rst.inc new file mode 100644 index 00000000000..a199364068e --- /dev/null +++ b/cookbook/security/_supportsToken.rst.inc @@ -0,0 +1,10 @@ +After Symfony calls ``createToken``, it will then call ``supportsToken`` on +your class (and any other authentication listeners) to figure out who should +handle it. This is just a way to allow several authentication mechanisms to +be used for the same firewall (that way, you can for instance first try to +authenticate the user via a certificate or an API key and fall back to a +form login). + +Mostly, you just need to make sure that this method returns ``true`` for a +token that has been created by ``createToken``. Your logic should probably +look exactly like this example. \ No newline at end of file diff --git a/cookbook/security/api_key_authentication.rst b/cookbook/security/api_key_authentication.rst index 48aa3f0aa20..26bc5b67bcf 100644 --- a/cookbook/security/api_key_authentication.rst +++ b/cookbook/security/api_key_authentication.rst @@ -6,7 +6,7 @@ How to Authenticate Users with API Keys 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 a HTTP header. +passed as a query string parameter or via an HTTP header. The API Key Authenticator ------------------------- @@ -16,7 +16,11 @@ 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\\Core\\Authentication\\SimplePreAuthenticatorInterface` -interface allows to implement such a scheme really easily:: +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/Acme/HelloBundle/Security/ApiKeyAuthenticator.php namespace Acme\HelloBundle\Security; @@ -26,7 +30,6 @@ interface allows to implement such a scheme really easily:: use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken; use Symfony\Component\HttpFoundation\Request; - use Symfony\Component\Security\Core\User\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\BadCredentialsException; @@ -55,22 +58,18 @@ interface allows to implement such a scheme really easily:: public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { - $apikey = $token->getCredentials(); - if (!$this->userProvider->getUsernameForApiKey($apikey)) { + $apiKey = $token->getCredentials(); + if (!$username = $this->userProvider->getUsernameForApiKey($apiKey)) { throw new AuthenticationException( - sprintf('API Key "%s" does not exist.', $apikey) + sprintf('API Key "%s" does not exist.', $apiKey) ); } - $user = new User( - $this->userProvider->getUsernameForApiKey($apikey), - $apikey, - array('ROLE_USER') - ); + $user = $this->userProvider->loadUserByUsername($username); return new PreAuthenticatedToken( $user, - $apikey, + $apiKey, $providerKey, $user->getRoles() ); @@ -82,17 +81,101 @@ interface allows to implement such a scheme really easily:: } } -``$userProvider`` can be any user provider implementing an interface similar to -this:: +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/admin/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 the +:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` +will cause authentication to fail. + +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. - // src/Acme/HelloBundle/Security/ApiKeyUserProviderInterface.php +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:`how to create a custom user 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 our 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/Acme/HelloBundle/Security/ApiKeyUserProvider.php namespace Acme\HelloBundle\Security; use Symfony\Component\Security\Core\User\UserProviderInterface; + use Symfony\Component\Security\Core\User\User; - interface ApiKeyUserProviderInterface extends UserProviderInterface + class ApiKeyUserProvider extends UserProviderInterface { - public function getUsernameForApiKey($apikey); + 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_USER') + ); + } + + 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 $class === 'Symfony\Component\Security\Core\User\User'; + } } .. note:: @@ -100,13 +183,38 @@ this:: Read the dedicated article to learn :doc:`how to create a custom user provider `. -To access a resource protected by such an authenticator, you need to add an apikey -parameter to the query string, like in ``http://example.com/admin/foo?apikey=37b51d194a7513e45b56f6524f2d51f2``. +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`` return ``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:`cookbook-security-api-key-session`. + +.. _cookbook-security-api-key-config: Configuration ------------- -Configure your ``ApiKeyAuthenticator`` as a service: +Once you have your ``ApiKeyAuthentication`` all setup, you need to register +it as a service and use it in ``security.yml``. First, register it as a service. +This assumes that you have already setup your custom user provider as a service +called ``your_api_key_user_provider`` (see :doc:`/cookbook/security/custom_provider`). .. configuration-block:: @@ -118,7 +226,7 @@ Configure your ``ApiKeyAuthenticator`` as a service: apikey_authenticator: class: Acme\HelloBundle\Security\ApiKeyAuthenticator - arguments: [@your_api_key_user_provider] + arguments: ["@your_api_key_user_provider"] .. code-block:: xml @@ -152,20 +260,23 @@ Configure your ``ApiKeyAuthenticator`` as a service: array(new Reference('your_api_key_user_provider')) )); -Then, activate it in your firewalls section using the ``simple-preauth`` key -like this: +Now, activate it in the ``firewalls`` section of ``security.yml`` using the +``simple_preauth`` key: .. configuration-block:: .. code-block:: yaml + # app/config/security.yml security: - firewalls: - secured_area: - pattern: ^/admin - simple-preauth: - provider: ... - authenticator: apikey_authenticator + # ... + + firewalls: + secured_area: + pattern: ^/admin + stateless: true + simple_preauth: + authenticator: apikey_authenticator .. code-block:: xml @@ -181,7 +292,7 @@ like this: @@ -198,11 +309,228 @@ like this: 'firewalls' => array( 'secured_area' => array( 'pattern' => '^/admin', - 'provider' => 'authenticator', - 'simple-preauth' => array( - 'provider' => ..., + 'stateless' => true, + 'simple_preauth' => array( 'authenticator' => 'apikey_authenticator', ), ), ), )); + +That's it! Now, your ``ApiKeyAuthentication`` 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! + +.. _cookbook-security-api-key-session: + +Storing Authentication in the Session +------------------------------------- + +So far, this entry 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 + + # app/config/security.yml + security: + # ... + + firewalls: + secured_area: + pattern: ^/admin + stateless: false + simple_preauth: + authenticator: apikey_authenticator + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/security.php + + // .. + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'secured_area' => array( + 'pattern' => '^/admin', + 'stateless' => false, + 'simple_preauth' => array( + 'authenticator' => 'apikey_authenticator', + ), + ), + ), + )); + +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/Acme/HelloBundle/Security/ApiKeyUserProvider.php + + // ... + + class ApiKeyUserProvider extends 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:`/cookbook/security/entity_provider`. + +Only Authenticating for certain URLs +------------------------------------ + +This entry 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/Acme/HelloBundle/Security/ApiKeyAuthenticator.php + + // ... + use Symfony\Component\Security\Http\HttpUtils; + + class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface + { + protected $userProvider; + + protected $httpUtils; + + public function __construct(ApiKeyUserProviderInterface $userProvider, HttpUtils $httpUtils) + { + $this->userProvider = $userProvider; + $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 (!$this->httpUtils->checkRequestPath($request, $targetUrl)) { + return; + } + + // ... + } + } + +This uses a handy :class:`Symfony\\Component\\Security\\Http\\HttpUtils` +class to see if the current URL matches the URL you're looking for. In this +case, the URL (``/login/check``) has been hardcoded in the class, but you +could also inject it as the third constructor argument. + +Next, just update your service configuration to inject the ``security.http_utils`` +service: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + services: + # ... + + apikey_authenticator: + class: Acme\HelloBundle\Security\ApiKeyAuthenticator + arguments: ["@your_api_key_user_provider", "@security.http_utils"] + + .. code-block:: xml + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + + $container->setDefinition('apikey_authenticator', new Definition( + 'Acme\HelloBundle\Security\ApiKeyAuthenticator', + array( + new Reference('your_api_key_user_provider'), + new Reference('security.http_utils') + ) + )); + +That's it! Have fun! diff --git a/cookbook/security/custom_authentication_provider.rst b/cookbook/security/custom_authentication_provider.rst index 009d34a401a..8b67ea391de 100644 --- a/cookbook/security/custom_authentication_provider.rst +++ b/cookbook/security/custom_authentication_provider.rst @@ -4,6 +4,15 @@ How to create a custom Authentication Provider ============================================== +.. tip:: + + Creating a custom authentication system is hard, and this entry will walk + you through that process. But depending on your needs, you may be able + to use solve your problem in a simpler way using these documents: + + * :doc:`/cookbook/security/custom_password_authenticator` + * :doc:`/cookbook/security/api_key_authentication` + If you have read the chapter on :doc:`/book/security`, you understand the distinction Symfony2 makes between authentication and authorization in the implementation of security. This chapter discusses the core classes involved diff --git a/cookbook/security/custom_password_authenticator.rst b/cookbook/security/custom_password_authenticator.rst index 2560bd52223..ef9f9be035f 100644 --- a/cookbook/security/custom_password_authenticator.rst +++ b/cookbook/security/custom_password_authenticator.rst @@ -1,12 +1,13 @@ .. index:: single: Security; Custom Password Authenticator -How to create a Custom Password Authenticator -============================================= +How to create a Custom Form Password Authenticator +================================================== -Imagine you want to allow access to your website only between 2pm and 4pm (for -the UTC timezone). Before Symfony 2.4, you had to create a custom token, factory, -listener and provider. +Imagine you want to allow access to your website only between 2pm and 4pm +UTC. Before Symfony 2.4, you had to create a custom token, factory, listener +and provider. In this entry, you'll learn how to do this for a login form +(i.e. where your user submits their username and password). The Password Authenticator -------------------------- @@ -14,10 +15,10 @@ The Password Authenticator .. versionadded:: 2.4 The ``SimpleFormAuthenticatorInterface`` interface was added in Symfony 2.4. -But now, thanks to new simplified authentication customization options in -Symfony 2.4, you don't need to create a whole bunch of new classes, but use the -:class:`Symfony\\Component\\Security\\Core\\Authentication\\SimpleFormAuthenticatorInterface` -interface instead:: +First, create a new class that implements +:class:`Symfony\\Component\\Security\\Core\\Authentication\\SimpleFormAuthenticatorInterface`. +Eventually, this will allow you to create custom logic for authenticating +the user:: // src/Acme/HelloBundle/Security/TimeAuthenticator.php namespace Acme\HelloBundle\Security; @@ -90,18 +91,43 @@ interface instead:: How it Works ------------ -There are a lot of things going on: +Great! Now you just need to setup some :ref:`cookbook-security-password-authenticator-config`. +But first, you can find out more about what each method in this class does. -* ``createToken()`` creates a Token that will be used to authenticate the user; -* ``authenticateToken()`` checks that the Token is allowed to log in by first - getting the User via the user provider and then, by checking the password - and the current time (a Token with roles is authenticated); -* ``supportsToken()`` is just a way to allow several authentication mechanisms to - be used for the same firewall (that way, you can for instance first try to - authenticate the user via a certificate or an API key and fall back to a - form login); -* An encoder is needed to check the user password's validity; this is a - service provided by default:: +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 checks 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, an encoder is needed to check the password's validity:: $encoder = $this->encoderFactory->getEncoder($user); $passwordValid = $encoder->isPasswordValid( @@ -110,6 +136,12 @@ There are a lot of things going on: $user->getSalt() ); +This is a service that is already available in Symfony and the password algorithm +is configured in ``security.yml`` under the ``encoders`` key. Below, you'll +see how to inject that into the ``TimeAuthenticator``. + +.. _cookbook-security-password-authenticator-config: + Configuration ------------- @@ -125,7 +157,7 @@ Now, configure your ``TimeAuthenticator`` as a service: time_authenticator: class: Acme\HelloBundle\Security\TimeAuthenticator - arguments: [@security.encoder_factory] + arguments: ["@security.encoder_factory"] .. code-block:: xml @@ -159,8 +191,8 @@ Now, configure your ``TimeAuthenticator`` as a service: array(new Reference('security.encoder_factory')) )); -Then, activate it in your ``firewalls`` section using the ``simple-form`` key -like this: +Then, activate it in the ``firewalls`` section of ``security.yml`` using +the ``simple_form`` key: .. configuration-block:: @@ -173,9 +205,8 @@ like this: firewalls: secured_area: pattern: ^/admin - provider: authenticator - simple-form: - provider: ... + # ... + simple_form: authenticator: time_authenticator check_path: login_check login_path: login @@ -194,7 +225,7 @@ like this: + > array( 'secured_area' => array( 'pattern' => '^/admin', - 'provider' => 'authenticator', - 'simple-form' => array( + 'simple_form' => array( 'provider' => ..., 'authenticator' => 'time_authenticator', 'check_path' => 'login_check', @@ -223,3 +253,10 @@ like this: ), ), )); + +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:`/cookbook/security/form_login`. From 6da1df65eaed400d4c6ebabdc90ed9ac1a937535 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Wed, 1 Jan 2014 19:24:47 -0600 Subject: [PATCH 2/2] [#3357] Many many changes thanks to @WouterJ and @xabbuh --- cookbook/security/_supportsToken.rst.inc | 16 ++-- cookbook/security/api_key_authentication.rst | 87 ++++++++++--------- .../custom_authentication_provider.rst | 2 +- .../custom_password_authenticator.rst | 28 +++--- 4 files changed, 69 insertions(+), 64 deletions(-) diff --git a/cookbook/security/_supportsToken.rst.inc b/cookbook/security/_supportsToken.rst.inc index a199364068e..aede5833cfb 100644 --- a/cookbook/security/_supportsToken.rst.inc +++ b/cookbook/security/_supportsToken.rst.inc @@ -1,10 +1,10 @@ -After Symfony calls ``createToken``, it will then call ``supportsToken`` on -your class (and any other authentication listeners) to figure out who should -handle it. This is just a way to allow several authentication mechanisms to -be used for the same firewall (that way, you can for instance first try to -authenticate the user via a certificate or an API key and fall back to a -form login). +After Symfony calls ``createToken()``, it will then call ``supportsToken()`` +on your class (and any other authentication listeners) to figure out who should +handle the token. This is just a way to allow several authentication mechanisms +to be used for the same firewall (that way, you can for instance first try +to authenticate the user via a certificate or an API key and fall back to +a form login). Mostly, you just need to make sure that this method returns ``true`` for a -token that has been created by ``createToken``. Your logic should probably -look exactly like this example. \ No newline at end of file +token that has been created by ``createToken()``. Your logic should probably +look exactly like this example. diff --git a/cookbook/security/api_key_authentication.rst b/cookbook/security/api_key_authentication.rst index 26bc5b67bcf..cd2812ebd88 100644 --- a/cookbook/security/api_key_authentication.rst +++ b/cookbook/security/api_key_authentication.rst @@ -20,7 +20,7 @@ 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:: +value and then a User object is created:: // src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php namespace Acme\HelloBundle\Security; @@ -59,7 +59,9 @@ value, and then a User object is created:: public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { $apiKey = $token->getCredentials(); - if (!$username = $this->userProvider->getUsernameForApiKey($apiKey)) { + $username = $this->userProvider->getUsernameForApiKey($apiKey) + + if (!$username) { throw new AuthenticationException( sprintf('API Key "%s" does not exist.', $apiKey) ); @@ -91,10 +93,10 @@ probably differ: 1. createToken ~~~~~~~~~~~~~~ -Early in the request cycle, Symfony calls ``createToken``. Your job here +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 the +parameter). If that information is missing, throwing a :class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` will cause authentication to fail. @@ -106,11 +108,11 @@ will cause authentication to fail. 3. authenticateToken ~~~~~~~~~~~~~~~~~~~~ -If ``supportsToken`` returns ``true``, Symfony will now call ``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``: +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``; @@ -119,18 +121,18 @@ In this specific example, the following things happen in ``authenticateToken``: #. 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 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:`how to create a custom user provider `). +The ``$userProvider`` can be any user provider (see :doc:`/cookbook/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 our use-case (i.e. this isn't a method that's +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:: @@ -140,6 +142,8 @@ The ``$userProvider`` might look something like this:: 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 extends UserProviderInterface { @@ -147,7 +151,7 @@ The ``$userProvider`` might look something like this:: { // Look up the username based on the token in the database, via // an API call, or do something entirely different - $username = // ... + $username = ...; return $username; } @@ -174,7 +178,7 @@ The ``$userProvider`` might look something like this:: public function supportsClass($class) { - return $class === 'Symfony\Component\Security\Core\User\User'; + return 'Symfony\Component\Security\Core\User\User' === $class; } } @@ -183,23 +187,23 @@ The ``$userProvider`` might look something like this:: 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 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 +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. +would allow you to have custom data on the ``User`` object. -Finally, just make sure that ``supportsClass`` return ``true`` for User -objects with the same class as whatever user you return in ``loadUserByUsername``. +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``. +exception in ``refreshUser()``. .. note:: @@ -212,9 +216,10 @@ Configuration ------------- Once you have your ``ApiKeyAuthentication`` all setup, you need to register -it as a service and use it in ``security.yml``. First, register it as a service. -This assumes that you have already setup your custom user provider as a service -called ``your_api_key_user_provider`` (see :doc:`/cookbook/security/custom_provider`). +it as a service and use it in your security configuration (e.g. ``security.yml``). +First, register it as a service. This assumes that you have already setup +your custom user provider as a service called ``your_api_key_user_provider`` +(see :doc:`/cookbook/security/custom_provider`). .. configuration-block:: @@ -260,8 +265,8 @@ called ``your_api_key_user_provider`` (see :doc:`/cookbook/security/custom_provi array(new Reference('your_api_key_user_provider')) )); -Now, activate it in the ``firewalls`` section of ``security.yml`` using the -``simple_preauth`` key: +Now, activate it in the ``firewalls`` section of your security configuration +using the ``simple_preauth`` key: .. configuration-block:: @@ -395,32 +400,31 @@ configuration or set it to ``false``: 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. + 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 +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:: +``refreshUser()`` should now return the User object:: // src/Acme/HelloBundle/Security/ApiKeyUserProvider.php // ... - class ApiKeyUserProvider extends UserProviderInterface { // ... public function refreshUser(UserInterface $user) { - // $user is the User that you set in the token inside authenticateToken + // $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(); + // $id = $user->getId(); // use $id to make a query // if you are *not* reading from a database and are just creating @@ -431,12 +435,12 @@ of the user to make sure it's not out-of-date. But regardless of your requiremen .. 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 + 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:`/cookbook/security/entity_provider`. -Only Authenticating for certain URLs +Only Authenticating for Certain URLs ------------------------------------ This entry has assumed that you want to look for the ``apikey`` authentication @@ -445,12 +449,13 @@ 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``:: +current URL is before creating the token in ``createToken()``:: // src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php // ... use Symfony\Component\Security\Http\HttpUtils; + use Symfony\Component\HttpFoundation\Request; class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface { @@ -477,8 +482,8 @@ current URL is before creating the token in ``createToken``:: } } -This uses a handy :class:`Symfony\\Component\\Security\\Http\\HttpUtils` -class to see if the current URL matches the URL you're looking for. In this +This uses the handy :class:`Symfony\\Component\\Security\\Http\\HttpUtils` +class to check if the current URL matches the URL you're looking for. In this case, the URL (``/login/check``) has been hardcoded in the class, but you could also inject it as the third constructor argument. diff --git a/cookbook/security/custom_authentication_provider.rst b/cookbook/security/custom_authentication_provider.rst index 8b67ea391de..b77b978fe11 100644 --- a/cookbook/security/custom_authentication_provider.rst +++ b/cookbook/security/custom_authentication_provider.rst @@ -8,7 +8,7 @@ How to create a custom Authentication Provider Creating a custom authentication system is hard, and this entry will walk you through that process. But depending on your needs, you may be able - to use solve your problem in a simpler way using these documents: + to solve your problem in a simpler way using these documents: * :doc:`/cookbook/security/custom_password_authenticator` * :doc:`/cookbook/security/api_key_authentication` diff --git a/cookbook/security/custom_password_authenticator.rst b/cookbook/security/custom_password_authenticator.rst index ef9f9be035f..2f4b86c4647 100644 --- a/cookbook/security/custom_password_authenticator.rst +++ b/cookbook/security/custom_password_authenticator.rst @@ -1,7 +1,7 @@ .. index:: single: Security; Custom Password Authenticator -How to create a Custom Form Password Authenticator +How to Create a Custom Form Password Authenticator ================================================== Imagine you want to allow access to your website only between 2pm and 4pm @@ -97,12 +97,12 @@ 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 +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`` +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``. +Whatever token object you create here will be passed to you later in ``authenticateToken()``. 2) supportsToken ~~~~~~~~~~~~~~~~ @@ -112,20 +112,20 @@ Whatever token object you create here will be passed to you later in ``authentic 3) authenticateToken ~~~~~~~~~~~~~~~~~~~~ -If ``supportsToken`` returns ``true``, Symfony will now call ``authenticateToken``. -Your job here is to checks that the token is allowed to log in by first -getting the User object via the user provider and then, by checking the password +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 "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. +(i.e. it has at least 1 role set on it) and which has the ``User`` object +inside of it. Inside this method, an encoder is needed to check the password's validity:: @@ -137,8 +137,8 @@ Inside this method, an encoder is needed to check the password's validity:: ); This is a service that is already available in Symfony and the password algorithm -is configured in ``security.yml`` under the ``encoders`` key. Below, you'll -see how to inject that into the ``TimeAuthenticator``. +is configured in the security configuration (e.g. ``security.yml``) under +the ``encoders`` key. Below, you'll see how to inject that into the ``TimeAuthenticator``. .. _cookbook-security-password-authenticator-config: @@ -191,8 +191,8 @@ Now, configure your ``TimeAuthenticator`` as a service: array(new Reference('security.encoder_factory')) )); -Then, activate it in the ``firewalls`` section of ``security.yml`` using -the ``simple_form`` key: +Then, activate it in the ``firewalls`` section of the security configuration +using the ``simple_form`` key: .. configuration-block::