Silex, el manual oficial

A.7. SecurityServiceProvider

El proveedor SecurityServiceProvider gestiona la autenticación y la autorización de acceso a tus aplicaciones.

A.7.1. Parámetros de configuración

  • security.hide_user_not_found (opcional): indica si se debe ocultar la excepción de tipo "Usuario no encontrado" o no. Por defecto es true.

A.7.2. Servicios proporcionados

El proveedor proporciona los siguientes servicios:

  • security: es el principal punto de acceso al proveedor de seguridad. Se emplea sobre todo para obtener el token que representa al usuario actual (solo a partir de Symfony 2.5).
  • security.token_storage: proporciona acceso al token del usuario (a partir de Symfony 2.6).
  • security.authorization_checker: permite comprobar la autorización que un usuario tiene sobre un recurso (a partir de Symfony 2.6).
  • security.authentication_manager: es una instancia de la clase AuthenticationProviderManager que se encarga de la autenticación.
  • security.access_manager: es una instancia de la clase AccessDecisionManager, que se encarga de todo lo relativo a la autorización.
  • security.session_strategy: define la estrategia que se utiliza en las sesiones relacionadas con la autenticación (por defecto se usa la estrategia migration).
  • security.user_checker: comprueba los permisos del usuario después de realizar la autenticación.
  • security.last_error: devuelve el último error de autenticación relacionado con el objeto Request proporcionado.
  • security.encoder_factory: define el mecanismo de codificación utilizado para las contraseñas de los usuarios (por defecto se utiliza un algoritmo básico de tipo digest).
  • security.encoder.digest: el codificador de contraseñas utilizado para todos los usuarios de la aplicación.
  • user: devuelve la instancia que representa al usuario actual.

Nota Este proveedor define muchos otros servicios que se utilizan internamente y por tanto, rara vez se necesita modificarlos por parte de la aplicación.

A.7.3. Cómo se registra el proveedor

El siguiente código muestra un ejemplo de cómo registrar este proveedor:

$app->register(new Silex\Provider\SecurityServiceProvider(array(
    'security.firewalls' => // ver explicación en la próxima sección
)));

Nota El componente Security se incluye cuando descargas Silex en forma de archivo comprimido. Si instalas Silex mediante Composer, debes añadir esta dependencia ejecutando el siguiente comando:

$ composer require symfony/security

Advertencia Las opciones de seguridad sólo están disponibles después de que la aplicación se haya iniciado. Así que si quieres utilizar todo eso fuera del controlador que procesa las peticiones de los usuarios, no olvides ejecutar primero el método boot() para iniciar la aplicación:

$app->boot();

Advertencia Si utilizas un formulario para autenticar usuarios, debes activar también el proveedor SessionServiceProvider.

A.7.4. Ejemplos de uso

El componente Security es uno de los más poderosos y complejos de Symfony. Consulta la documentación relacionada con el componente para aprender más sobre el.

Truco Si tu aplicación no funciona como esperas, es posible que la configuración de la seguridad no sea correcta. Para solucionar más fácilmente este tipo de problemas, no olvides activar los logs en la aplicación, ya que el componente Security genera muchos mensajes de log útiles. Para activar el log, consulta la sección anterior sobre el proveedor MonologServiceProvider.

A continuación se incluye una lista con las soluciones a los problemas más habituales relacionados con la seguridad de la aplicación.

A.7.4.1. Acceder al objeto que representa el usuario actual

La información del usuario que está actualmente conectado a la aplicación se guarda en un objeto especial llamado token. Puedes acceder a este token a través del servicio security:

// Symfony 2.6+
$token = $app['security.token_storage']->getToken();

// Symfony 2.3/2.5
$token = $app['security']->getToken();

Si la aplicación no dispone de información sobre el usuario, el valor del token es null. Si un usuario está conectado a la aplicación, puedes obtener su objeto a través del método getUser() del token:

if (null !== $token) {
    $user = $token->getUser();
}

El valor devuelto por el método getUser() puede ser una cadena de texto, un objeto que implemente el método __toString(), o una instancia de la clase UserInterface.

A.7.4.2. Restringir el acceso a una parte del sitio web

El siguiente código muestra un ejemplo que utiliza la autenticación simple de HTTP para proteger con contraseña cualquier URL de la aplicación que empiece por /admin/:

$app['security.firewalls'] = array(
    'admin' => array(
        'pattern' => '^/admin',
        'http' => true,
        'users' => array(
            // la contraseña sin codificar es "foo"
            'admin' => array('ROLE_ADMIN', '5FZ2Z8QIkA7UTZ4BYkoC+GsReLf569mSKDsfods6LYQ8t+a8EW9oaircfMpmaLbPBh4FOBiiFyLfuZmTSUwzZg=='),
        ),
    ),
);

El valor de la opción pattern es una expresión regular que debe cumplir la URL para estar protegida por el firewall. La opción http con valor true indica que se debe utilizar la autenticación simple de HTTP y la opción users define los usuarios de la aplicación (en este caso se define solo un usuario cuyo login es admin).

Si quieres restringir el firewall más allá de una simple URL (por ejemplo restringir por método HTTP, IP del usuario, nombre del host, etc.) debes usar una instancia de la clase RequestMatcher en la opción pattern:

use Symfony/Component/HttpFoundation/RequestMatcher;

$app['security.firewalls'] = array(
    'admin' => array(
        'pattern' => new RequestMatcher('^/admin', 'example.com', 'POST'),
        // ...
    ),
);

Para cada usuario de la aplicación se debe proporcionar la siguiente información:

  • El rol o array de roles asociados al usuario (un rol es una cadena de texto que empieza por ROLE_y sigue con el valor que quieras).
  • La contraseña codificada del usuario.

Advertencia Todos los usuarios deben tener al menos un rol asociado.

Por defecto Silex obliga a que las contraseñas de los usuarios estén codificadas (por eso en el ejemplo anterior hay una cadena muy larga de caracteres extraños). Para codificar una contraseña, utiliza el servicio security.encoder_factory:

// encuentra el codificador adecuado para la instancia de UserInterface
$encoder = $app['security.encoder_factory']->getEncoder($user);

// codificar la contraseña "foo"
$password = $encoder->encodePassword('foo', $user->getSalt());

Cuando el usuario de la aplicación se ha autenticado, el usuario almacenado en el token es una instancia de la clase User.

Advertencia Si utilizas php-cgi con Apache, debes añadir la siguiente configuración para que la seguridad funcione correctamente:

RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ app.php [QSA,L]

A.7.4.3. Restringir con un formulario el acceso a una parte del sitio web

Si prefieres utilizar un formulario para que los usuarios se autentiquen, la configuración es muy parecida a la mostrada anteriormente. El principal cambio es que en vez de la opción http, ahora debes definir la opción form y estos dos parámetros de configuración:

  • login_path: la ruta a la que se le redirige al usuario cuando intenta acceder a una zona restringida y todavía no se ha autenticado.
  • check_path: la URL del controlador que valida que el login y contraseña introducidos por el usuario son correctos.

El siguiente código muestra cómo proteger con un formulario todas las URL que empiezan por /admin/:

$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=='),
        ),
    ),
);

A la hora de elegir las rutas asociadas con el formulario, ten en cuenta lo siguiente:

  • La ruta a la que apunta la opción login_path debe encontrarse fuera de la zona protegida de la aplicación. Si estuviera dentro de una zona protegida, asegúrate de activar el mecanismo de autenticación anonymous, tal y como se explica más adelante.
  • La ruta a la que apunta la opción check_path siempre debe encontrase dentro de la zona protegida de la aplicación.

Para completar el ejemplo anterior, define el controlador que crea el formulario de login:

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'),
    ));
});

La variable error contiene el último mensaje de error relacionado con la autenticación (si existe) y la variable last_username almacena el último login que ha probado el usuario antes de producirse el error.

Por último, crea la plantilla para mostrar el formulario

<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>

Nota La ruta admin_login_check la crea Silex automáticamente y su nombre se crea utilizando el valor de la opción check_path (las barras inclinadas / se reemplazan por guiones bajos _ y la primera barra / se elimina).

A.7.4.4. Definiendo más de un firewall

Las aplicaciones Silex pueden definir más de un firewall por cada proyecto.

Definir varios firewalls puede ser utili cuando quieres utilizar diferentes estrategias de autenticación en varias partes del sitio web o cuando quieres utilizar diferentes tipos de usuarios (por ejemplo utilizando la autenticación HTTP básica para la API del sitio y un formulario para la zona privada de administración del sitio web).

Si quieres proteger todas las URL de la aplicación salvo el formulario de login, también tendrás que definir más de un firewall:

$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=='),
        ),
    ),
);

El orden en el que se registran los firewalls es muy importante, ya que siempre se utiliza el primero cuyo patrón coincide con la URL solicitada por el usuario. Así que la configuración anterior primero hace que la URL /login no sea segura (ya que no tiene ninguna información de autenticación) y después protege el resto de URL de la aplicación.

Truco La opción security permite activar o desactivar la autenticación del firewall:

$app['security.firewalls'] = array(
    'api' => array(
        'pattern' => '^/api',
        'security' => $app['debug'] ? false : true,
        'wsse' => true,
        // ...
    ),
);

A.7.4.5. Desconectando usuarios

Cuando utilices un formulario para autenticar a los usuarios, puedes añadir la opción logout para permitir que los usuarios se puedan desconectar del sitio. El valor de la opción logout_path debe encontrarse dentro de la zona protegida del sitio web:

$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
        ),

        // ...
    ),
);

En este caso también se genera automáticamente una ruta en función del valor de la opción logout_path (las barras inclinadas / se reemplazan por guiones bajos _ y la primera barra / se elimina).:

<a href="{{ path('logout') }}">Logout</a>

A.7.4.6. Permitiendo el acceso a los usuarios anónimos

Si en tu aplicación sólo está protegido el acceso a algunas zonas concretas, la información del usuario no estará disponible en las zonas no seguras. Para poder obtener la información del usuario también en esas zonas, activa el mecanismo de autenticación anonymous:

$app['security.firewalls'] = array(
    'unsecured' => array(
        'anonymous' => true,

        // ...
    ),
);

Después de activar esta opción, el usuario actual siempre está disponible a través del componente de seguridad. Si el usuario no está autenticado, Silex devuelve la cadena anon. para hacer referencia a los usuarios anónimos.

A.7.4.7. Comprobando los roles de los usuarios

Utiliza el método isGranted() para comprobar si un usuario tiene un determinado rol:

// Symfony 2.6+
if ($app['security.authorization_checker']->isGranted('ROLE_ADMIN')) {
    // ...
}

// Symfony 2.3/2.5
if ($app['security']->isGranted('ROLE_ADMIN')) {
    // ...
}

Esta comprobación también se puede realizar desde las plantillas Twig:

{% if is_granted('ROLE_ADMIN') %}
    <a href="/secured?_switch_user=fabien">Switch to Fabien</a>
{% endif %}

Silex define un rol especial llamado IS_AUTHENTICATED_FULLY que permite comprobar si el usuario está realmente autenticado, es decir, si no es anónimo:

{% if is_granted('IS_AUTHENTICATED_FULLY') %}
    <a href="{{ path('logout') }}">Logout</a>
{% else %}
    <a href="{{ path('login') }}">Login</a>
{% endif %}

Para que el código anterior funcione, no olvides definir una ruta llamada login.

Truco No utilices el método getRoles() para comprobar los roles del usuario.

Advertencia El método isGranted() lanza una excepción cuando no dispone de la información relativa a la autenticación (lo que sucede en las zonas no protegidas). Esto significa que si utilizas por ejemplo este método en la plantilla de una página no protegida por un firewall, Silex mostrará un mensaje de error.

A.7.4.8. Impersonando usuarios

Silex también permite impersonar usuarios, lo que te permite ver la aplicación como si fueras otro usuario (y sin necesidad de saber su contraseña). Para ello, activa la estrategia de autenticación switch_user:

$app['security.firewalls'] = array(
    'unsecured' => array(
        'switch_user' => array(
            'parameter' => '_switch_user',
            'role' => 'ROLE_ALLOWED_TO_SWITCH'
        ),

        // ...
    ),
);

Para cambiar de usuario, añade el parámetro _switch_user a cualquier URL indicando el login del usuario por el que te quieres hacer pasar (para que puedas hacerlo, tu usuario tiene que tener el rol ROLE_ALLOWED_TO_SWITCH):

{% if is_granted('ROLE_ALLOWED_TO_SWITCH') %}
    <a href="?_switch_user=fabien">Hacerte pasar por el usuario Fabien</a>
{% endif %}

Silex define otro rol especial llamado ROLE_PREVIOUS_ADMIN para comprobar si el usuario actual en realidad es otro usuario que está haciéndose pasar por el. Este rol sirve por ejemplo para que el usuario anule la impersonación y vuelva a ser él mismo:

{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
    Eres un usuario de tipo administrador pero te has cambiado a otro usuario,
    <a href="?_switch_user=_exit"> anular</a> el cambio de usuario.
{% endif %}

A.7.4.9. Definiendo una jerarquía de roles

Las jerarquías de roles o permisos permiten asignar automáticamente roles a los usuarios. Observa el siguiente código:

$app['security.role_hierarchy'] = array(
    'ROLE_ADMIN' => array('ROLE_USER', 'ROLE_ALLOWED_TO_SWITCH'),
);

Esta configuración hace que todos los usuarios que tengan el rol ROLE_ADMIN también dispongan automáticamente de los roles ROLE_USER y ROLE_ALLOWED_TO_SWITCH.

A.7.4.10. Configurando las reglas de acceso

Los roles permiten a la aplicación dividir a los usuarios en diferentes tipos, pero también se emplean para resringir el acceso a determinadas zonas del sitio web:

$app['security.access_rules'] = array(
    array('^/admin', 'ROLE_ADMIN', 'https'),
    array('^.*$', 'ROLE_USER'),
);

La configuración anterior obliga a que los usuarios que quieran acceder a cualquier URL que empiece por /admin tengan que disponer del rol ROLE_ADMIN. Para acceder al resto del sitio web, los usuarios deben disponer del rol ROLE_USER. Además, la opción https indica que la sección de administración sólo puede ser accedida a través de HTTPS. Si no es el caso, Silex redirigirá automáticamente al usuario.

Nota El primer valor de cada array, además de una cadena de texto, también puede ser una instancia de la clase RequestMatcher.

A.7.4.11. Utilizando un proveedor de usuarios propio

Los arrays de usuarios son una manera sencilla de definir los usuarios que pueden acceder a la aplicación. Esta solución puede ser correcta por ejemplo para la parte de administración de un sitio web personal o muy pequeño. No obstante, para sitios web medianos o grandes es mejor definir tu propio proveedor de usuarios.

El valor de la opción users también puede ser un servicio que devuelva una instancia de la clase UserProviderInterface:

'users' => $app->share(function () use ($app) {
    return new UserProvider($app['db']);
}),

El siguiente código muestra un ejemplo de un proveedor de usuarios que utiliza una base de datos para almacenar los usuarios (para ello utiliza la librería DBAL de Doctrine):

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';
    }
}

Como puedes ver en el código anterior, se utiliza la clase User para almacenar la información del usuario, pero también puedes utilizar tu propia clase. El único requisito es que la clase implemente la interfaz UserInterface.

Y este es el código que podrías utilizar para crear las tablas en la base de datos y para cargar algunos usuarios de prueba:

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'
    ));
}

Truco Si utilizas el ORM de Doctrine, puedes hacer uso de una clase proveedora de usuarios que los carga a partir de entidades de Doctrine.

A.7.4.12. Utilizando un codificador de contraseñas propio

Silex utiliza por defecto el algoritmo sha512 para codificar las contraseñas de los usuarios. El proceso es un poco complejo, ya que la contraseña se codifica varias veces seguidas utilizando cada vez el resultado del paso anterior. Por último, el resultado se codifica mediante la codificación base64. Para modificar estas opciones, redefine el servicio security.encoder.digest:

use Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

$app['security.encoder.digest'] = $app->share(function ($app) {
    // utilizar el algoritmo sha1, sin codificar el resultado en base64 y
    // realizando 1 única iteración
    return new MessageDigestPasswordEncoder('sha1', false, 1);
});

A.7.4.13. Utilizando un proveedor de autenticación propio

El componente Security de Symfony incluye muchos proveedores de autenticación listos para utilizar (HTTP, formularios, X.509, etc.) pero tamabién puedes crear tus propios proveedores.

Para registrar un nuevo proveedor de autenticación, crea un servicio llamado security.authentication_listener.factory.XXX, reemplazando el valor XXX por el nombre con el que quieras utilizar este proveedor en la configuración de tu aplicación. El siguiente ejemplo crea un proveedor llamado wsse:

$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'
    );
});

Una vez definido, ya puedes utilizar tu proveedor exactamente igual que si fuera alguno de los proveedores predefinidos de Symfony:

$app->register(new Silex\Provider\SecurityServiceProvider(), array(
    'security.firewalls' => array(
        'default' => array(
            'wsse' => true,

            // ...
        ),
    ),
));

El valor true se puede reemplazar por un array de opciones para ajustar el comportamiento del proveedor de autenticación. Este array se pasa al constructor del proveedor como segundo argumento.

A.7.4.14. Autenticación sin estado

Por defecto se genera una cookie para guardar el contexto de seguridad del usuario. Sin embargo, si utilizas certificados digitales, autenticación HTTP, WSSE, etc. las credenciales del usuario se envían en cada petición. En esos casos, puedes desactivar la persistencia basada en cookies mediante la opción stateless:

$app['security.firewalls'] = array(
    'default' => array(
        'stateless' => true,
        'wsse' => true,

        // ...
    ),
);

A.7.5. Traits

El trait Silex\Application\SecurityTrait definido por este proveedor añade los siguientes atajos:

  • encodePassword: codifica la contraseña indicada.
$user = $app->user();

$encoded = $app->encodePassword($user, 'foo');

El trait Silex\Route\SecurityTrait definido por este proveedor añade los siguientes atajos:

  • secure: protege un controlador para que solo pueda ser accedido por los roles indicados.
$app->get('/', function () {
    // hacer algo relacionado con los usuarios de tipo admin
})->secure('ROLE_ADMIN');

Advertencia Ten en cuenta que Silex\Route\SecurityTrait se utiliza con una clase propia de tipo Route, no con la aplicación.

use Silex\Route;

class MyRoute extends Route
{
    use Route\SecurityTrait;
}

// ...

$app['route_class'] = 'MyRoute';