diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index 0f689f32f53..7917f2e1a54 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -162,6 +162,7 @@ * :doc:`/cookbook/security/form_login_setup` * :doc:`/cookbook/security/entity_provider` + * :doc:`/cookbook/security/guard-authentication` * :doc:`/cookbook/security/remember_me` * :doc:`/cookbook/security/impersonating_user` * :doc:`/cookbook/security/form_login` diff --git a/cookbook/security/api_key_authentication.rst b/cookbook/security/api_key_authentication.rst index f2e9fc7e824..da7de4f8502 100644 --- a/cookbook/security/api_key_authentication.rst +++ b/cookbook/security/api_key_authentication.rst @@ -4,6 +4,11 @@ How to Authenticate Users with API Keys ======================================= +.. tip:: + + Check out :doc:`/cookbook/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. diff --git a/cookbook/security/custom_authentication_provider.rst b/cookbook/security/custom_authentication_provider.rst index 0d57295277a..f44ea0b8cf8 100644 --- a/cookbook/security/custom_authentication_provider.rst +++ b/cookbook/security/custom_authentication_provider.rst @@ -10,6 +10,7 @@ How to Create a custom Authentication Provider you through that process. But depending on your needs, you may be able to solve your problem in a simpler, or via a community bundle: + * :doc:`/cookbook/security/guard-authentication` * :doc:`/cookbook/security/custom_password_authenticator` * :doc:`/cookbook/security/api_key_authentication` * To authenticate via OAuth using a third-party service such as Google, Facebook diff --git a/cookbook/security/custom_password_authenticator.rst b/cookbook/security/custom_password_authenticator.rst index c8d54869c11..c71995b7402 100644 --- a/cookbook/security/custom_password_authenticator.rst +++ b/cookbook/security/custom_password_authenticator.rst @@ -4,6 +4,11 @@ How to Create a Custom Form Password Authenticator ================================================== +.. tip:: + + Check out :doc:`/cookbook/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. 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 diff --git a/cookbook/security/guard-authentication.rst b/cookbook/security/guard-authentication.rst new file mode 100644 index 00000000000..5201fcf2bbb --- /dev/null +++ b/cookbook/security/guard-authentication.rst @@ -0,0 +1,579 @@ +.. index:: + single: Security; Custom Authentication + +How to Create a Custom Authentication System with Guard +======================================================= + +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. + +Create a User and a User Provider +--------------------------------- + +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:: + + // src/AppBundle/Entity/User.php + namespace AppBundle\Entity; + + use Symfony\Component\Security\Core\User\UserInterface; + use Doctrine\ORM\Mapping as ORM; + + /** + * @ORM\Entity + * @ORM\Table(name="user") + */ + class User implements UserInterface + { + /** + * @ORM\Id + * @ORM\GeneratedValue(strategy="AUTO") + * @ORM\Column(type="integer") + */ + private $id; + + /** + * @ORM\Column(type="string", unique=true) + */ + private $username; + + /** + * @ORM\Colum(type"string", unique=true) + */ + private $apiKey; + + public function getUsername() + { + return $this->username; + } + + public function getRoles() + { + return ['ROLE_USER']; + } + + public function getPassword() + { + } + public function getSalt() + { + } + public function eraseCredentials() + { + } + + // more getters/setters + } + +.. 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 + + # app/config/security.yml + security: + # ... + + providers: + your_db_provider: + entity: + class: AppBundle:User + + # ... + + .. code-block:: xml + + + + + + + + + + + + + + + + + .. code-block:: php + + // app/config/security.php + $container->loadFromExtension('security', array( + // ... + + 'providers' => array( + 'your_db_provider' => array( + 'entity' => array( + 'class' => 'AppBundle:User', + ), + ), + ), + + // ... + )); + +That's it! Need more information about this step, see: + +* :doc:`/cookbook/security/entity_provider` +* :doc:`/cookbook/security/custom_provider` + +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\\GuardAuthenticatorInterface`. Or, extend +the simpler :class:`Symfony\\Component\\Security\\Guard\\AbstractGuardAuthenticator`. +This requires you to implement six methods:: + + // src/AppBundle/Security/TokenAuthenticator.php + namespace AppBundle\Security; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\HttpFoundation\JsonResponse; + use Symfony\Component\Security\Core\User\UserInterface; + use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + use Symfony\Component\Security\Core\Exception\AuthenticationException; + use Symfony\Component\Security\Core\User\UserProviderInterface; + use Doctrine\ORM\EntityManager; + + class TokenAuthenticator extends AbstractGuardAuthenticator + { + private $em; + + public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Called on every request. Return whatever credentials you want, + * or null to stop authentication. + */ + public function getCredentials(Request $request) + { + if (!$token = $request->headers->get('X-AUTH-TOKEN')) { + // no token? Return null and no other methods will be called + return; + } + + // What you return here will be passed to getUser() as $credentials + return array( + 'token' => $token, + ); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $apiToken = $credentials['token']; + + // if null, authentication will fail + // if a User object, checkCredentials() is called + return $this->em->getRepository('AppBundle:User') + ->findOneBy(array('apiToken' => $apiToken)); + } + + public function checkCredentials($credentials, UserInterface $user) + { + // check credentials - e.g. make sure the password is valid + // no credential check is needed in this case + + // return true to cause authentication success + return true; + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + // on success, let the request continue + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + $data = array( + 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) + + // or to translate this message + // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) + ); + + return new JsonResponse($data, 403); + } + + /** + * Called when authentication is needed, but it's not sent + */ + public function start(Request $request, AuthenticationException $authException = null) + { + $data = array( + // you might translate this message + 'message' => 'Authentication Required' + ); + + return new JsonResponse($data, 401); + } + + public function supportsRememberMe() + { + return false; + } + } + +Nice work! Each method is explained below: :ref:`The Guard Authenticator Methods`. + +Step 2) Configure the Authenticator +----------------------------------- + +To finish this, register the class as a service: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/services.yml + services: + app.token_authenticator: + class: AppBundle\Security\TokenAuthenticator + arguments: ['@doctrine.orm.entity_manager'] + + .. code-block:: xml + + + + + + + + + .. code-block:: php + + // app/config/services.php + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + $container->setDefinition('app.token_authenticator', new Definition( + 'AppBundle\Security\TokenAuthenticator', + array(new Reference('doctrine.orm.entity_manager')) + )); + +Finally, configure your ``firewalls`` key in ``security.yml`` to use this authenticator: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + # ... + + firewalls: + # ... + + main: + anonymous: ~ + logout: ~ + + guard: + authenticators: + - app.token_authenticator + + # if you want, disable storing the user in the session + # stateless: true + + # maybe other things, like form_login, remember_me, etc + # ... + + .. code-block:: xml + + + + + + + + + + + + app.token_authenticator + + + + + + + + .. code-block:: php + + // app/config/security.php + + // .. + + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'main' => array( + 'pattern' => '^/', + 'anonymous' => true, + 'logout' => true, + 'guard' => array( + 'authenticators' => array( + 'app.token_authenticator' + ), + ), + // ... + ), + ), + )); + +You did it! You now have a fully-working API token authentication system. If your +homepage required ``ROLE_USER``, then you could test it under different conditions: + +.. code-block:: bash + + # test with no token + curl http://localhost:8000/ + # {"message":"Authentication Required"} + + # test with a bad token + curl -H "X-AUTH-TOKEN: FAKE" http://localhost:8000/ + # {"message":"Username could not be found."} + + # test with a working token + curl -H "X-AUTH-TOKEN: REAL" http://localhost:8000/ + # the homepage controller is executed: the page loads normally + +Now, learn more about what each method does. + +.. _guard-auth-methods: + +The Guard Authenticator Methods +------------------------------- + +Each authenticator needs the following methods: + +**getCredentials(Request $request)** + This will be called on *every* request and your job is to read the token (or + whatever your "authentication" information is) from the request and return it. + If you return ``null``, the rest of the authentication process is skipped. Otherwise, + ``getUser()`` will be called and the return value is passed as the first argument. + +**getUser($credentials, UserProviderInterface $userProvider)** + If ``getCredentials()`` returns a non-null value, then this method is called + and its return value is passed here as the ``$credentials`` argument. Your job + is to return an object that implements ``UserInterface``. If you do, then + ``checkCredentials()`` will be called. If you return ``null`` (or throw an + :ref:`AuthenticationException `) + authentication will fail. + +**checkCredentials($credentials, UserInterface $user)** + If ``getUser()`` returns a User object, this method is called. Your job is to + verify if the credentials are correct. For a login form, this is where you would + check that the password is correct for the user. To pass authentication, return + ``true``. If you return *anything* else + (or throw an :ref:`AuthenticationException `), + authentication will fail. + +**onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)** + This is called after successful authentication and your job is to either + return a :class:`Symfony\\Component\\HttpFoundation\\Response` object + that will be sent to the client or ``null`` to continue the request + (e.g. allow the route/controller to be called like normal). Since this + is an API where each request authenticates itself, you want to return + ``null``. + +**onAuthenticationFailure(Request $request, AuthenticationException $exception)** + This is called if authentication fails. Your job + is to return the :class:`Symfony\\Component\\HttpFoundation\\Response` + object that should be sent to the client. The ``$exception`` will tell you + *what* went wrong during authentication. + +**start** + This is called if the client accesses a URI/resource that requires authentication, + but no authentication details were sent (i.e. you returned ``null`` from + ``getCredentials()``). Your job is to return a + :class:`Symfony\\Component\\HttpFoundation\\Response` object that helps + the user authenticate (e.g. a 401 response that says "token is missing!"). + +**supportsRememberMe** + If you want to support "remember me" functionality, return true from this method. + You will still need to active ``rememebe_me`` under your firewall for it to work. + Since this is a stateless API, you do not want to support "remember me" + functionality in this example. + +.. _guard-customize-error: + +Customizing Error Messages +-------------------------- + +When ``onAuthenticationFailure()`` is called, it is passed an ``AuthenticationException`` +that describes *how* authentication failed via its ``$e->getMessageKey()`` (and +``$e->getMessageData()``) method. The message will be different based on *where* +authentication fails (i.e. ``getUser()`` versus ``checkCredentials()``). + +But, you can easily return a custom message by throwing a +:class:`Symfony\\Component\\Security\\Core\\Exception\\CustomUserMessageAuthenticationException`. +You can throw this from ``getCredentials()``, ``getUser()`` or ``checkCredentials()`` +to cause a failure:: + + // src/AppBundle/Security/TokenAuthenticator.php + // ... + + use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; + + class TokenAuthenticator extends AbstractGuardAuthenticator + { + // ... + + public function getCredentials(Request $request) + { + // ... + + if ($token == 'ILuvAPIs') { + throw new CustomUserMessageAuthenticationException( + 'ILuvAPIs is not a real API key: it\'s just a silly phrase' + ); + } + + // ... + } + + // ... + } + +In this case, since "ILuvAPIs" is a ridiculous API key, you could include an easter +egg to return a custom message if someone tries this: + +.. code-block:: bash + + curl -H "X-AUTH-TOKEN: ILuvAPIs" http://localhost:8000/ + # {"message":"ILuvAPIs is not a real API key: it's just a silly phrase"} + +Frequently Asked Questions +-------------------------- + +**Can I have Multiple Authenticators?** + Yes! But when you do, you'll need 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 example, suppose you have an ``app.form_login_authenticator`` that handles + a traditional form login. When a user accesses a protected page anonymously, you + want to use the ``start()`` method from the form authenticator and redirect them + to the login page (instead of returning a JSON response): + + .. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + # ... + + firewalls: + # ... + + main: + anonymous: ~ + logout: ~ + + guard: + authenticators: + - app.token_authenticator + + # if you want, disable storing the user in the session + # stateless: true + + # maybe other things, like form_login, remember_me, etc + # ... + + .. code-block:: xml + + + + + + + + + + + + app.token_authenticator + + + + + + + + .. code-block:: php + + // app/config/security.php + + // .. + + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'main' => array( + 'pattern' => '^/', + 'anonymous' => true, + 'logout' => true, + 'guard' => array( + 'authenticators' => array( + 'app.token_authenticator' + ), + ), + // ... + ), + ), + )); + +**Can I use this with ``form_login``?** + Yes! ``form_login`` is *one* way to authenticate a user, so you could use + it *and* then add one or more authenticators. Using a guard authenticator doesn't + collide with other ways to authenticate. + +**Can I use this with FOSUserBundle?** + Yes! Actually, FOSUserBundle doesn't handle security: it simply gives you a + ``User`` object and some routes and controllers to help with login, registration, + forgot password, etc. When you use FOSUserBundle, you typically use ``form_login`` + to actually authenticate the user. You can continue doing that (see previous + question) or use the ``User`` object from FOSUserBundle and create your own + authenticator(s) (just like in this article). diff --git a/cookbook/security/index.rst b/cookbook/security/index.rst index c9a478c927a..bdbeaab10d8 100644 --- a/cookbook/security/index.rst +++ b/cookbook/security/index.rst @@ -9,6 +9,7 @@ Authentication (Identifying/Logging in the User) form_login_setup entity_provider + guard-authentication remember_me impersonating_user form_login