diff --git a/cookbook/security/_supportsToken.rst.inc b/cookbook/security/_supportsToken.rst.inc new file mode 100644 index 00000000000..aede5833cfb --- /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 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. diff --git a/cookbook/security/api_key_authentication.rst b/cookbook/security/api_key_authentication.rst index 48aa3f0aa20..cd2812ebd88 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,20 @@ 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(); + $username = $this->userProvider->getUsernameForApiKey($apiKey) + + if (!$username) { 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 +83,103 @@ 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 a +:class:`Symfony\\Component\\Security\\Core\\Exception\\BadCredentialsException` +will cause authentication to fail. + +2. supportsToken +~~~~~~~~~~~~~~~~ + +.. include:: _supportsToken.rst.inc + +3. authenticateToken +~~~~~~~~~~~~~~~~~~~~ - // src/Acme/HelloBundle/Security/ApiKeyUserProviderInterface.php +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:`/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 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/Acme/HelloBundle/Security/ApiKeyUserProvider.php namespace Acme\HelloBundle\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; - 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 'Symfony\Component\Security\Core\User\User' === $class; + } } .. note:: @@ -100,13 +187,39 @@ 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()`` 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:`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 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:: @@ -118,7 +231,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 +265,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 your security configuration +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 +297,7 @@ like this: @@ -198,11 +314,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; + use Symfony\Component\HttpFoundation\Request; + + 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 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. + +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..b77b978fe11 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 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..2f4b86c4647 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 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, 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 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: + 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 the security configuration +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`.