Skip to content
This repository has been archived by the owner on Jul 4, 2018. It is now read-only.

Commit

Permalink
feature #1296 Add support for the Guard component to the SecurityServ…
Browse files Browse the repository at this point in the history
…iceProvider (GromNaN)

This PR was squashed before being merged into the 2.0.x-dev branch (closes #1296).

Discussion
----------

Add support for the Guard component to the SecurityServiceProvider

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #1260
| License       | MIT
| Doc PR        | included

Services configuration are extracted from the SecurityBundle [`guard.xml`](https://github.com/symfony/symfony/blob/3.0/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml) and [`GuardAuthenticationFactory`](https://github.com/symfony/symfony/blob/3.0/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php)

Usage is quite simple, the `guard` type can be configured in the firewall like other ones.

```php
$app['app.authenticator'] = function ($app) {
    return new Authenticator();
};

$app->register(new Silex\Provider\SecurityServiceProvider(), [
    'security.firewalls' => [
        'main' => [
            'pattern' => '^/admin',
            'guard' => [
                'authenticators' => [
                    'app.authenticator'
                ]
            ]
        ]
    ]
]);
```

Commits
-------

4b5ccc9 Add support for the Guard component to the SecurityServiceProvider
  • Loading branch information
fabpot committed Apr 29, 2016
2 parents a8cc0ae + 4b5ccc9 commit bade8a0
Show file tree
Hide file tree
Showing 5 changed files with 400 additions and 7 deletions.
182 changes: 182 additions & 0 deletions doc/cookbook/guard_authentication.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
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.

Step 1) Create the Authenticator Class
--------------------------------------

Suppose you have an API where your clients will send an X-AUTH-TOKEN
header on each request. This token is composed of the username followed
by a password, separated by a colon (e.g. ``X-AUTH-TOKEN: coolguy:awesomepassword``).
Your job is to read this, find theassociated user (if any) and check
the password.

To create a custom authentication system, just create a class and make
it implement GuardAuthenticatorInterface. Or, extend the simpler
AbstractGuardAuthenticator. This requires you to implement six methods:

.. code-block:: php
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
class TokenAuthenticator extends AbstractGuardAuthenticator
{
private $encoderFactory;
public function __construct(EncoderFactoryInterface $encoderFactory)
{
$this->encoderFactory = $encoderFactory;
}
public function getCredentials(Request $request)
{
// Checks if the credential header is provided
if (!$token = $request->headers->get('X-AUTH-TOKEN')) {
return;
}
// Parse the header or ignore it if the format is incorrect.
if (false === strpos(':', $token)) {
return;
}
list($username, $secret) = explode(':', $token, 2);
return array(
'username' => $username,
'secret' => $secret,
);
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
return $userProvider->loadUserByUsername($credentials['username']);
}
public function checkCredentials($credentials, UserInterface $user)
{
// check credentials - e.g. make sure the password is valid
// return true to cause authentication success
$encoder = $this->encoderFactory->getEncoder($user);
return $encoder->isPasswordValid(
$user->getPassword(),
$credentials['secret'],
$user->getSalt()
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
{
// on success, let the request continue
return;
}
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;
}
}
Step 2) Configure the Authenticator
-----------------------------------

To finish this, register the class as a service:

.. code-block:: php
$app['app.token_authenticator'] = function ($app) {
return new App\Security\TokenAuthenticator($app['security.encoder_factory']);
};
Finally, configure your `security.firewalls` key to use this authenticator:

.. code-block:: php
$app['security.firewalls'] => array(
'main' => array(
'guard' => array(
'authenticators' => array(
'app.token_authenticator'
),
// Using more than 1 authenticator, you must specify
// which one is used as entry point.
// 'entry_point' => 'app.token_authenticator',
),
// configure where your users come from. Hardcode them, or load them from somewhere
// http://silex.sensiolabs.org/doc/providers/security.html#defining-a-custom-user-provider
'users' => array(
'victoria' => array('ROLE_USER', 'randomsecret'),
),
// 'anonymous' => true
),
);
.. note::
You can use many authenticators, they are executed by the order
they are configured.

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: alan" http://localhost:8000/
# {"message":"Username could not be found."}
# test with a working token
curl -H "X-AUTH-TOKEN: victoria:ransomsecret" http://localhost:8000/
# the homepage controller is executed: the page loads normally
For more details read the Symfony cookbook entry on
`How to Create aCustom Authentication System with Guard <http://symfony.com/doc/current/cookbook/security/guard-authentication.html>`_.
7 changes: 7 additions & 0 deletions doc/providers/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,13 @@ argument of your authentication factory (see above).
This example uses the authentication provider classes as described in the
Symfony `cookbook`_.


.. note::

Since Symfony 2.8, the Guard component simplify the creation of custom
authentication providers.
:doc:`How to Create a Custom Authentication System with Guard <cookbook/guard_authentication>`

Stateless Authentication
~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
78 changes: 71 additions & 7 deletions src/Silex/Provider/SecurityServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
use Symfony\Component\Security\Http\Logout\DefaultLogoutSuccessHandler;
use Symfony\Component\Security\Http\AccessMap;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
use Symfony\Component\Security\Guard\Firewall\GuardAuthenticationListener;
use Symfony\Component\Security\Guard\Provider\GuardAuthenticationProvider;

/**
* Symfony Security component Provider.
Expand Down Expand Up @@ -164,12 +167,14 @@ public function register(Container $app)
};

// generate the build-in authentication factories
foreach (array('logout', 'pre_auth', 'form', 'http', 'remember_me', 'anonymous') as $type) {
foreach (array('logout', 'pre_auth', 'guard', 'form', 'http', 'remember_me', 'anonymous') as $type) {
$entryPoint = null;
if ('http' === $type) {
$entryPoint = 'http';
} elseif ('form' === $type) {
$entryPoint = 'form';
} elseif ('guard' === $type) {
$entryPoint = 'guard';
}

$app['security.authentication_listener.factory.'.$type] = $app->protect(function ($name, $options) use ($type, $app, $entryPoint) {
Expand All @@ -181,9 +186,14 @@ public function register(Container $app)
$app['security.authentication_listener.'.$name.'.'.$type] = $app['security.authentication_listener.'.$type.'._proto']($name, $options);
}

$provider = 'anonymous' === $type ? 'anonymous' : 'dao';
$provider = 'dao';
if ('anonymous' === $type) {
$provider = 'anonymous';
} elseif ('guard' === $type) {
$provider = 'guard';
}
if (!isset($app['security.authentication_provider.'.$name.'.'.$provider])) {
$app['security.authentication_provider.'.$name.'.'.$provider] = $app['security.authentication_provider.'.$provider.'._proto']($name);
$app['security.authentication_provider.'.$name.'.'.$provider] = $app['security.authentication_provider.'.$provider.'._proto']($name, $options);
}

return array(
Expand All @@ -196,7 +206,7 @@ public function register(Container $app)
}

$app['security.firewall_map'] = function ($app) {
$positions = array('logout', 'pre_auth', 'form', 'http', 'remember_me', 'anonymous');
$positions = array('logout', 'pre_auth', 'guard', 'form', 'http', 'remember_me', 'anonymous');
$providers = array();
$configs = array();
foreach ($app['security.firewalls'] as $name => $firewall) {
Expand Down Expand Up @@ -301,7 +311,7 @@ public function register(Container $app)
$listener = $app[$listenerId];

if (isset($app['security.remember_me.service.'.$name])) {
if ($listener instanceof AbstractAuthenticationListener) {
if ($listener instanceof AbstractAuthenticationListener || $listener instanceof GuardAuthenticationListener) {
$listener->setRememberMeServices($app['security.remember_me.service.'.$name]);
}
if ($listener instanceof LogoutListener) {
Expand Down Expand Up @@ -445,6 +455,27 @@ public function register(Container $app)
};
});

$app['security.authentication_listener.guard._proto'] = $app->protect(function ($providerKey, $options) use ($app, $that) {
return function () use ($app, $providerKey, $options, $that) {
if (!isset($app['security.authentication.guard_handler'])) {
$app['security.authentication.guard_handler'] = new GuardAuthenticatorHandler($app['security.token_storage'], $app['dispatcher']);
}

$authenticators = array();
foreach ($options['authenticators'] as $authenticatorId) {
$authenticators[] = $app[$authenticatorId];
}

return new GuardAuthenticationListener(
$app['security.authentication.guard_handler'],
$app['security.authentication_manager'],
$providerKey,
$authenticators,
$app['logger']
);
};
});

$app['security.authentication_listener.form._proto'] = $app->protect(function ($name, $options) use ($app, $that) {
return function () use ($app, $name, $options, $that) {
$that->addFakeRoute(
Expand Down Expand Up @@ -570,7 +601,24 @@ public function register(Container $app)
};
});

$app['security.authentication_provider.dao._proto'] = $app->protect(function ($name) use ($app) {
$app['security.entry_point.guard._proto'] = $app->protect(function ($name, array $options) use ($app) {
if (isset($options['entry_point'])) {
// if it's configured explicitly, use it!
return $app[$options['entry_point']];
}
$authenticatorIds = $options['authenticators'];
if (count($authenticatorIds) == 1) {
// if there is only one authenticator, use that as the entry point
return $app[reset($authenticatorIds)];
}
// we have multiple entry points - we must ask them to configure one
throw new \LogicException(sprintf(
'Because you have multiple guard configurators, you need to set the "guard.entry_point" key to one of you configurators (%s)',
implode(', ', $authenticatorIds)
));
});

$app['security.authentication_provider.dao._proto'] = $app->protect(function ($name, $options) use ($app) {
return function () use ($app, $name) {
return new DaoAuthenticationProvider(
$app['security.user_provider.'.$name],
Expand All @@ -582,7 +630,23 @@ public function register(Container $app)
};
});

$app['security.authentication_provider.anonymous._proto'] = $app->protect(function ($name) use ($app) {
$app['security.authentication_provider.guard._proto'] = $app->protect(function ($name, $options) use ($app) {
return function () use ($app, $name, $options) {
$authenticators = array();
foreach ($options['authenticators'] as $authenticatorId) {
$authenticators[] = $app[$authenticatorId];
}

return new GuardAuthenticationProvider(
$authenticators,
$app['security.user_provider.'.$name],
$name,
$app['security.user_checker']
);
};
});

$app['security.authentication_provider.anonymous._proto'] = $app->protect(function ($name, $options) use ($app) {
return function () use ($app, $name) {
return new AnonymousAuthenticationProvider($name);
};
Expand Down
Loading

0 comments on commit bade8a0

Please sign in to comment.