The SecurityServiceProvider manages authentication and authorization for your applications.
true
.Note
The service provider defines many other services that are used internally but rarely need to be customized.
1 2 3 | $app->register(new Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => // see below
));
|
Note
The Symfony Security Component comes with the "fat" Silex archive but not with the regular one. If you are using Composer, add it as a dependency:
1 | composer require symfony/security
|
Caution
The security features are only available after the Application has been
booted. So, if you want to use it outside of the handling of a request,
don't forget to call boot()
first:
$app->boot();
Caution
If you're using a form to authenticate users, you need to enable
SessionServiceProvider
.
The Symfony Security component is powerful. To learn more about it, read the Symfony Security documentation.
Tip
When a security configuration does not behave as expected, enable logging (with the Monolog extension for instance) as the Security Component logs a lot of interesting information about what it does and why.
Below is a list of recipes that cover some common use cases.
The current user information is stored in a token that is accessible via the
security
service:
1 2 3 4 5 | // Symfony 2.6+
$token = $app['security.token_storage']->getToken();
// Symfony 2.3/2.5
$token = $app['security']->getToken();
|
If there is no information about the user, the token is null
. If the user
is known, you can get it with a call to getUser()
:
if (null !== $token) {
$user = $token->getUser();
}
The user can be a string, an object with a __toString()
method, or an
instance of UserInterface.
The following configuration uses HTTP basic authentication to secure URLs
under /admin/
:
1 2 3 4 5 6 7 8 9 10 | $app['security.firewalls'] = array(
'admin' => array(
'pattern' => '^/admin',
'http' => true,
'users' => array(
// raw password is foo
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
),
);
|
The pattern
is a regular expression on the URL path; the http
setting
tells the security layer to use HTTP basic authentication and the users
entry defines valid users.
If you want to restrict the firewall by more than the URL pattern (like the
HTTP method, the client IP, the hostname, or any Request attributes), use an
instance of a RequestMatcher
for the pattern
option:
1 2 3 4 5 6 7 8 | use Symfony/Component/HttpFoundation/RequestMatcher;
$app['security.firewalls'] = array(
'admin' => array(
'pattern' => new RequestMatcher('^/admin', 'example.com', 'POST'),
// ...
),
);
|
Each user is defined with the following information:
ROLE_
and ending with anything you want);Caution
All users must at least have one role associated with them.
The default configuration of the extension enforces encoded passwords. To
generate a valid encoded password from a raw password, use the
security.encoder_factory
service:
1 2 3 4 5 | // find the encoder for a UserInterface instance
$encoder = $app['security.encoder_factory']->getEncoder($user);
// compute the encoded password for foo
$password = $encoder->encodePassword('foo', $user->getSalt());
|
When the user is authenticated, the user stored in the token is an instance of User
Caution
If you are using php-cgi under Apache, you need to add this configuration to make things work correctly:
1 2 3 4 5 | RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ app.php [QSA,L]
|
Using a form to authenticate users is very similar to the above configuration.
Instead of using the http
setting, use the form
one and define these
two parameters:
Here is how to secure all URLs under /admin/
with a form:
1 2 3 4 5 6 7 8 9 | $app['security.firewalls'] = array(
'admin' => array(
'pattern' => '^/admin/',
'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'),
'users' => array(
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
),
);
|
Always keep in mind the following two golden rules:
login_path
path must always be defined outside the secured area
(or if it is in the secured area, the anonymous
authentication mechanism
must be enabled -- see below);check_path
path must always be defined inside the secured area.For the login form to work, create a controller like the following:
1 2 3 4 5 6 7 8 | use Symfony\Component\HttpFoundation\Request;
$app->get('/login', function(Request $request) use ($app) {
return $app['twig']->render('login.html', array(
'error' => $app['security.last_error']($request),
'last_username' => $app['session']->get('_security.last_username'),
));
});
|
The error
and last_username
variables contain the last authentication
error and the last username entered by the user in case of an authentication
error.
Create the associated template:
1 2 3 4 5 6 | <form action="{{ path('admin_login_check') }}" method="post">
{{ error }}
<input type="text" name="_username" value="{{ last_username }}" />
<input type="password" name="_password" value="" />
<input type="submit" />
</form>
|
Note
The admin_login_check
route is automatically defined by Silex and its
name is derived from the check_path
value (all /
are replaced with
_
and the leading /
is stripped).
You are not limited to define one firewall per project.
Configuring several firewalls is useful when you want to secure different parts of your website with different authentication strategies or for different users (like using an HTTP basic authentication for the website API and a form to secure your website administration area).
It's also useful when you want to secure all URLs except the login form:
1 2 3 4 5 6 7 8 9 10 11 12 | $app['security.firewalls'] = array(
'login' => array(
'pattern' => '^/login$',
),
'secured' => array(
'pattern' => '^.*$',
'form' => array('login_path' => '/login', 'check_path' => '/login_check'),
'users' => array(
'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
),
),
);
|
The order of the firewall configurations is significant as the first one to
match wins. The above configuration first ensures that the /login
URL is
not secured (no authentication settings), and then it secures all other URLs.
Tip
You can toggle all registered authentication mechanisms for a particular
area on and off with the security
flag:
1 2 3 4 5 6 7 8 9 | $app['security.firewalls'] = array(
'api' => array(
'pattern' => '^/api',
'security' => $app['debug'] ? false : true,
'wsse' => true,
// ...
),
);
|
When using a form for authentication, you can let users log out if you add the
logout
setting, where logout_path
must match the main firewall
pattern:
1 2 3 4 5 6 7 8 9 | $app['security.firewalls'] = array(
'secured' => array(
'pattern' => '^/admin/',
'form' => array('login_path' => '/login', 'check_path' => '/admin/login_check'),
'logout' => array('logout_path' => '/admin/logout', 'invalidate_session' => true),
// ...
),
);
|
A route is automatically generated, based on the configured path (all /
are replaced with _
and the leading /
is stripped):
1 | <a href="{{ path('admin_logout') }}">Logout</a>
|
When securing only some parts of your website, the user information are not
available in non-secured areas. To make the user accessible in such areas,
enabled the anonymous
authentication mechanism:
1 2 3 4 5 6 7 | $app['security.firewalls'] = array(
'unsecured' => array(
'anonymous' => true,
// ...
),
);
|
When enabling the anonymous setting, a user will always be accessible from the
security context; if the user is not authenticated, it returns the anon.
string.
To check if a user is granted some role, use the isGranted()
method on the
security context:
1 2 3 4 5 6 7 8 9 | // Symfony 2.6+
if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) {
// ...
}
// Symfony 2.3/2.5
if ($app['security']->isGranted('ROLE_ADMIN')) {
// ...
}
|
You can check roles in Twig templates too:
1 2 3 | {% if is_granted('ROLE_ADMIN') %}
<a href="/secured?_switch_user=fabien">Switch to Fabien</a>
{% endif %}
|
You can check if a user is "fully authenticated" (not an anonymous user for
instance) with the special IS_AUTHENTICATED_FULLY
role:
1 2 3 4 5 | {% if is_granted('IS_AUTHENTICATED_FULLY') %}
<a href="{{ path('logout') }}">Logout</a>
{% else %}
<a href="{{ path('login') }}">Login</a>
{% endif %}
|
Of course you will need to define a login
route for this to work.
Tip
Don't use the getRoles()
method to check user roles.
Caution
isGranted()
throws an exception when no authentication information is
available (which is the case on non-secured area).
If you want to be able to switch to another user (without knowing the user
credentials), enable the switch_user
authentication strategy:
1 2 3 4 5 6 7 | $app['security.firewalls'] = array(
'unsecured' => array(
'switch_user' => array('parameter' => '_switch_user', 'role' => 'ROLE_ALLOWED_TO_SWITCH'),
// ...
),
);
|
Switching to another user is now a matter of adding the _switch_user
query
parameter to any URL when logged in as a user who has the
ROLE_ALLOWED_TO_SWITCH
role:
1 2 3 | {% if is_granted('ROLE_ALLOWED_TO_SWITCH') %}
<a href="?_switch_user=fabien">Switch to user Fabien</a>
{% endif %}
|
You can check that you are impersonating a user by checking the special
ROLE_PREVIOUS_ADMIN
. This is useful for instance to allow the user to
switch back to their primary account:
1 2 3 4 | {% if is_granted('ROLE_PREVIOUS_ADMIN') %}
You are an admin but you've switched to another user,
<a href="?_switch_user=_exit"> exit</a> the switch.
{% endif %}
|
Defining a role hierarchy allows to automatically grant users some additional roles:
$app['security.role_hierarchy'] = array(
'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'),
);
With this configuration, all users with the ROLE_ADMIN
role also
automatically have the ROLE_USER
and ROLE_ALLOWED_TO_SWITCH
roles.
Roles are a great way to adapt the behavior of your website depending on groups of users, but they can also be used to further secure some areas by defining access rules:
$app['security.access_rules'] = array(
array('^/admin', 'ROLE_ADMIN', 'https'),
array('^.*$', 'ROLE_USER'),
);
With the above configuration, users must have the ROLE_ADMIN
to access the
/admin
section of the website, and ROLE_USER
for everything else.
Furthermore, the admin section can only be accessible via HTTPS (if that's not
the case, the user will be automatically redirected).
Note
The first argument can also be a RequestMatcher instance.
Using an array of users is simple and useful when securing an admin section of a personal website, but you can override this default mechanism with you own.
The users
setting can be defined as a service that returns an instance of
UserProviderInterface:
'users' => $app->share(function () use ($app) {
return new UserProvider($app['db']);
}),
Here is a simple example of a user provider, where Doctrine DBAL is used to store the users:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Doctrine\DBAL\Connection;
class UserProvider implements UserProviderInterface
{
private $conn;
public function __construct(Connection $conn)
{
$this->conn = $conn;
}
public function loadUserByUsername($username)
{
$stmt = $this->conn->executeQuery('SELECT * FROM users WHERE username = ?', array(strtolower($username)));
if (!$user = $stmt->fetch()) {
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
}
return new User($user['username'], $user['password'], explode(',', $user['roles']), true, true, true, true);
}
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return $class === 'Symfony\Component\Security\Core\User\User';
}
}
|
In this example, instances of the default User
class are created for the
users, but you can define your own class; the only requirement is that the
class must implement UserInterface
And here is the code that you can use to create the database schema and some sample users:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | use Doctrine\DBAL\Schema\Table;
$schema = $app['db']->getSchemaManager();
if (!$schema->tablesExist('users')) {
$users = new Table('users');
$users->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true));
$users->setPrimaryKey(array('id'));
$users->addColumn('username', 'string', array('length' => 32));
$users->addUniqueIndex(array('username'));
$users->addColumn('password', 'string', array('length' => 255));
$users->addColumn('roles', 'string', array('length' => 255));
$schema->createTable($users);
$app['db']->insert('users', array(
'username' => 'fabien',
'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
'roles' => 'ROLE_USER'
));
$app['db']->insert('users', array(
'username' => 'admin',
'password' => '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg==',
'roles' => 'ROLE_ADMIN'
));
}
|
Tip
If you are using the Doctrine ORM, the Symfony bridge for Doctrine provides a user provider class that is able to load users from your entities.
By default, Silex uses the sha512
algorithm to encode passwords.
Additionally, the password is encoded multiple times and converted to base64.
You can change these defaults by overriding the security.encoder.digest
service:
1 2 3 4 5 6 7 8 | use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;
$app['security.encoder.digest'] = $app->share(function ($app) {
// use the sha1 algorithm
// don't base64 encode the password
// use only 1 iteration
return new MessageDigestPasswordEncoder('sha1', false, 1);
});
|
The Symfony Security component provides a lot of ready-to-use authentication
providers (form, HTTP, X509, remember me, ...), but you can add new ones
easily. To register a new authentication provider, create a service named
security.authentication_listener.factory.XXX
where XXX
is the name you want to
use in your configuration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $app['security.authentication_listener.factory.wsse'] = $app->protect(function ($name, $options) use ($app) {
// define the authentication provider object
$app['security.authentication_provider.'.$name.'.wsse'] = $app->share(function () use ($app) {
return new WsseProvider($app['security.user_provider.default'], __DIR__.'/security_cache');
});
// define the authentication listener object
$app['security.authentication_listener.'.$name.'.wsse'] = $app->share(function () use ($app) {
// use 'security' instead of 'security.token_storage' on Symfony <2.6
return new WsseListener($app['security.token_storage'], $app['security.authentication_manager']);
});
return array(
// the authentication provider id
'security.authentication_provider.'.$name.'.wsse',
// the authentication listener id
'security.authentication_listener.'.$name.'.wsse',
// the entry point id
null,
// the position of the listener in the stack
'pre_auth'
);
});
|
You can now use it in your configuration like any other built-in authentication provider:
1 2 3 4 5 6 7 8 9 | $app->register(new Silex\Provider\SecurityServiceProvider(), array(
'security.firewalls' => array(
'default' => array(
'wsse' => true,
// ...
),
),
));
|
Instead of true
, you can also define an array of options that customize
the behavior of your authentication factory; it will be passed as the second
argument of your authentication factory (see above).
This example uses the authentication provider classes as described in the Symfony cookbook.
By default, a session cookie is created to persist the security context of
the user. However, if you use certificates, HTTP authentication, WSSE and so
on, the credentials are sent for each request. In that case, you can turn off
persistence by activating the stateless
authentication flag:
1 2 3 4 5 6 7 8 | $app['security.firewalls'] = array(
'default' => array(
'stateless' => true,
'wsse' => true,
// ...
),
);
|
Silex\Application\SecurityTrait
adds the following shortcuts:
1 2 3 | $user = $app->user();
$encoded = $app->encodePassword($user, 'foo');
|
Silex\Route\SecurityTrait
adds the following methods to the controllers:
1 2 3 | $app->get('/', function () {
// do something but only for admins
})->secure('ROLE_ADMIN');
|
Caution
The Silex\Route\SecurityTrait
must be used with a user defined
Route
class, not the application.
1 2 3 4 5 6 | use Silex\Route;
class MyRoute extends Route
{
use Route\SecurityTrait;
}
|
1 | $app['route_class'] = 'MyRoute';
|