diff --git a/cookbook/map.rst.inc b/cookbook/map.rst.inc index d98207e594d..cf4496ebee4 100644 --- a/cookbook/map.rst.inc +++ b/cookbook/map.rst.inc @@ -129,6 +129,8 @@ * :doc:`/cookbook/security/form_login` * :doc:`/cookbook/security/securing_services` * :doc:`/cookbook/security/custom_provider` + * :doc:`/cookbook/security/custom_password_authenticator` + * :doc:`/cookbook/security/api_key_authentication` * :doc:`/cookbook/security/custom_authentication_provider` * :doc:`/cookbook/security/target_path` diff --git a/cookbook/security/api_key_authentication.rst b/cookbook/security/api_key_authentication.rst new file mode 100644 index 00000000000..48aa3f0aa20 --- /dev/null +++ b/cookbook/security/api_key_authentication.rst @@ -0,0 +1,208 @@ +.. index:: + single: Security; Custom Request Authenticator + +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. + +The API Key Authenticator +------------------------- + +.. versionadded:: 2.4 + The ``SimplePreAuthenticatorInterface`` interface was added in Symfony 2.4. + +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:: + + // src/Acme/HelloBundle/Security/ApiKeyAuthenticator.php + namespace Acme\HelloBundle\Security; + + use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface; + use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + 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; + + class ApiKeyAuthenticator implements SimplePreAuthenticatorInterface + { + protected $userProvider; + + public function __construct(ApiKeyUserProviderInterface $userProvider) + { + $this->userProvider = $userProvider; + } + + public function createToken(Request $request, $providerKey) + { + if (!$request->query->has('apikey')) { + throw new BadCredentialsException('No API key found'); + } + + return new PreAuthenticatedToken( + 'anon.', + $request->query->get('apikey'), + $providerKey + ); + } + + public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) + { + $apikey = $token->getCredentials(); + if (!$this->userProvider->getUsernameForApiKey($apikey)) { + throw new AuthenticationException( + sprintf('API Key "%s" does not exist.', $apikey) + ); + } + + $user = new User( + $this->userProvider->getUsernameForApiKey($apikey), + $apikey, + array('ROLE_USER') + ); + + return new PreAuthenticatedToken( + $user, + $apikey, + $providerKey, + $user->getRoles() + ); + } + + public function supportsToken(TokenInterface $token, $providerKey) + { + return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey; + } + } + +``$userProvider`` can be any user provider implementing an interface similar to +this:: + + // src/Acme/HelloBundle/Security/ApiKeyUserProviderInterface.php + namespace Acme\HelloBundle\Security; + + use Symfony\Component\Security\Core\User\UserProviderInterface; + + interface ApiKeyUserProviderInterface extends UserProviderInterface + { + public function getUsernameForApiKey($apikey); + } + +.. note:: + + 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``. + +Configuration +------------- + +Configure your ``ApiKeyAuthenticator`` as a service: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + services: + # ... + + apikey_authenticator: + class: Acme\HelloBundle\Security\ApiKeyAuthenticator + arguments: [@your_api_key_user_provider] + + .. 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')) + )); + +Then, activate it in your firewalls section using the ``simple-preauth`` key +like this: + +.. configuration-block:: + + .. code-block:: yaml + + security: + firewalls: + secured_area: + pattern: ^/admin + simple-preauth: + provider: ... + authenticator: apikey_authenticator + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/security.php + + // .. + + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'secured_area' => array( + 'pattern' => '^/admin', + 'provider' => 'authenticator', + 'simple-preauth' => array( + 'provider' => ..., + 'authenticator' => 'apikey_authenticator', + ), + ), + ), + )); diff --git a/cookbook/security/custom_password_authenticator.rst b/cookbook/security/custom_password_authenticator.rst new file mode 100644 index 00000000000..2560bd52223 --- /dev/null +++ b/cookbook/security/custom_password_authenticator.rst @@ -0,0 +1,225 @@ +.. index:: + single: Security; Custom Password Authenticator + +How to create a Custom 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. + +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:: + + // src/Acme/HelloBundle/Security/TimeAuthenticator.php + namespace Acme\HelloBundle\Security; + + use Symfony\Component\HttpFoundation\Request; + use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface; + use Symfony\Component\Security\Core\Authentication\TokenInterface; + use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; + use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; + use Symfony\Component\Security\Core\Exception\AuthenticationException; + use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; + use Symfony\Component\Security\Core\User\UserProviderInterface; + + class TimeAuthenticator implements SimpleFormAuthenticatorInterface + { + private $encoderFactory; + + public function __construct(EncoderFactoryInterface $encoderFactory) + { + $this->encoderFactory = $encoderFactory; + } + + public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) + { + try { + $user = $userProvider->loadUserByUsername($token->getUsername()); + } catch (UsernameNotFoundException $e) { + throw new AuthenticationException('Invalid username or password'); + } + + $encoder = $this->encoderFactory->getEncoder($user); + $passwordValid = $encoder->isPasswordValid( + $user->getPassword(), + $token->getCredentials(), + $user->getSalt() + ); + + if ($passwordValid) { + $currentHour = date('G'); + if ($currentHour < 14 || $currentHour > 16) { + throw new AuthenticationException( + 'You can only log in between 2 and 4!', + 100 + ); + } + + return new UsernamePasswordToken( + $user->getUsername(), + $user->getPassword(), + $providerKey, + $user->getRoles() + ); + } + + throw new AuthenticationException('Invalid username or password'); + } + + public function supportsToken(TokenInterface $token, $providerKey) + { + return $token instanceof UsernamePasswordToken + && $token->getProviderKey() === $providerKey; + } + + public function createToken(Request $request, $username, $password, $providerKey) + { + return new UsernamePasswordToken($username, $password, $providerKey); + } + } + +How it Works +------------ + +There are a lot of things going on: + +* ``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:: + + $encoder = $this->encoderFactory->getEncoder($user); + $passwordValid = $encoder->isPasswordValid( + $user->getPassword(), + $token->getCredentials(), + $user->getSalt() + ); + +Configuration +------------- + +Now, configure your ``TimeAuthenticator`` as a service: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/config.yml + services: + # ... + + time_authenticator: + class: Acme\HelloBundle\Security\TimeAuthenticator + arguments: [@security.encoder_factory] + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/config.php + use Symfony\Component\DependencyInjection\Definition; + use Symfony\Component\DependencyInjection\Reference; + + // ... + + $container->setDefinition('time_authenticator', new Definition( + 'Acme\HelloBundle\Security\TimeAuthenticator', + array(new Reference('security.encoder_factory')) + )); + +Then, activate it in your ``firewalls`` section using the ``simple-form`` key +like this: + +.. configuration-block:: + + .. code-block:: yaml + + # app/config/security.yml + security: + # ... + + firewalls: + secured_area: + pattern: ^/admin + provider: authenticator + simple-form: + provider: ... + authenticator: time_authenticator + check_path: login_check + login_path: login + + .. code-block:: xml + + + + + + + + + + + + + + .. code-block:: php + + // app/config/security.php + + // .. + + $container->loadFromExtension('security', array( + 'firewalls' => array( + 'secured_area' => array( + 'pattern' => '^/admin', + 'provider' => 'authenticator', + 'simple-form' => array( + 'provider' => ..., + 'authenticator' => 'time_authenticator', + 'check_path' => 'login_check', + 'login_path' => 'login', + ), + ), + ), + )); diff --git a/cookbook/security/index.rst b/cookbook/security/index.rst index a8edbdc4317..d0856f144cf 100644 --- a/cookbook/security/index.rst +++ b/cookbook/security/index.rst @@ -13,5 +13,7 @@ Security form_login securing_services custom_provider + custom_password_authenticator + api_key_authentication custom_authentication_provider target_path