/PommGuardServiceProvider

Authentication and authorization manager for Silex

Primary LanguagePHP

PommGuardServiceProvider

== IMPORTANT ==

This is beta software and even if it's tested and working, it has not yet been tried on real life projects.

Installation

PommGuardServiceProvider can be installed either by composer or as git submodule. You can of course just download the archive and set your autoloader.

Setup

Database

In psql, import the sql/tables.sql script. It will create the pomm_guard schema with the tables needed.

        Table "pomm_guard.pomm_user"
  Column  |        Type         | Modifiers
----------+---------------------+-----------
 login    | character varying   | not null
 password | character varying   | not null
 groups   | character varying[] |
Indexes:
    "pomm_user_pkey" PRIMARY KEY, btree (login)
Check constraints:
    "check_groups" CHECK (groups_exist(groups))

         Table "pomm_guard.pomm_group"
   Column    |        Type         | Modifiers
-------------+---------------------+-----------
 name        | character varying   | not null
 credentials | character varying[] |
Indexes:
    "pomm_group_pkey" PRIMARY KEY, btree (name)

If you want to enable the automatic encryption, you must include the trigger.sql script. It requires the plpgsql language and the pgcrypto extension to be installed in the current database. Note you must be database superuser to do so.

=# CREATE LANGUAGE plpgsql;
CREATE LANGUAGE
=# -- PG 9.1
=# CREATE EXTENSION pgcrypto;
CREATE EXTENSION
=# -- <= PG 9.0
=# \i /usr/share/postgresql/8.4/contrib/pgcrypto.sql

Silex

Registering the provider is easy as:

$app->register(new \GHub\Silex\PommGuard\PommGuardServiceProvider());

This provider extends the Silex session provider so you might pass it all the parameters it accepts.

The following options are specific to pomm guard:

pomm_guard.config.login_url
Url non authenticated users will be redirected to when trying to reach protected content. (default /login)
pomm_guard.config.logout_url
Url authenticated users will be redirected to when trying to reach non-protected content. (default /logout)
pomm_guard.config.user
Fully qualified User's Pomm model class name. (default \GHub\Silex\PommGuard\Model\PommUser)
pomm_guard.config.connection
Connection instance, if none given a connection from the default database will be created on demand.

You can enforce a custom connection:

$app['pomm_guard.config.connection'] = $app->share(function() use ($app) {
    $logger = new Pomm\Tools\Logger();

    return $app['pomm']->getDatabase('plop')
        ->createConnection(array('identity_mapper' => 'Pomm\Identity\IdentityMapperStrict'))
        ->registerFilter(new Pomm\FilterChain\LoggerFilter($logger));
        });

This is useful when you want to use custom database or query filters like the logger.

Using the provider

Simple use

If the provided pomm_user and pomm_group tables fit your needs, you do not need anything to add in the database. If you want to enable automatic password encryption, you must create a trigger on the pomm_guard.pomm_user table:

CREATE TRIGGER before_insert_update_pomm_user
  BEFORE UPDATE OR INSERT ON pomm_guard.pomm_user
  FOR EACH ROW EXECUTE PROCEDURE pomm_guard.pomm_user_encrypt_password();

This way, all you have got to do is to insert or update rows giving plain text passwords, they will be encrypted on the fly:

$map = $connection
  ->getMapFor('GHub\Silex\PommGuard\Model\PommUser');

$user = $map->createObject(array(
    'login'     => 'pika',
    'password'  => 'chu'
));
$map->saveOne($user);

// This should retrieve the user
//
// using automatic encryption:
$auth_user = $map->checkPassword(
    array('login' => 'pika'),
    'chu');

// using plain text:
$auth_user = $map->checkPassword(
    array('login' => 'pika'),
    'chu',
    true);

In the database you should have something like the following:

=$ SELECT * FROM pomm_guard.pomm_user;
 login |              password              | groups
-------+------------------------------------+--------
 pika  | $1$fujKjHzg$IiAzmkm2SBLO/FqjuxFDZ0 |
(1 row)

Note that the password is removed from the fields returned by your SELECT statements so unless you specify differently, $user['password'] will not exist when fetched from the database.

PommGuard provides you with several functions to be used as middleware for your controllers:

must_be_authenticated()
return a redirection to $app['pomm_guard.config.login_url'] (default /login) if the current session is NOT authenticated.
must_not_be_authenticated()
return a redirection to $app['pomm_guard.config.login_url'] (default /logout) if the current session IS authenticated.
// This controller is protected from non authenticated access.
$app->get('/protected/url', function() use ($app) {
  ...
})->middleware($app['pomm_guard.must_be_authenticated']);

The service provider overrides the normal Session instance with its own. This class adds several methods dedicated to use with authentication and Pomm:

setUserMap(BaseObjectMap $instance)♢
Called in the register() method.
setPommUser(ModelPommUser $user)♢
Attach a user with the session.
removePommUser()♢
Remove the user from session.
getPommUser()♢
Retrieve the user from session.
authenticate($authenticate)♢
Set authenticated (true or false).
isAuthenticated()♢
Get session authenticated state.
hasCredential($credential)♢
Return true if given credential is set to the attached user.
hasCredentials(Array $credentials)♢
Return true if all given credentials are set to the attached user.

A default login controller would be like:

$app->post('/login', function() use ($app) {
    if ($app['request']->request->has('login')) {
        $login = $app['request']->request->get('login');
        $user = $app['pomm.connection']
            ->getMapFor('Db\Schema\YourUser')
            ->checkPassword(array('login' => $login['email']), $login['password']);

        if (!is_null($user)) {
            $app['session']->setPommUser($user);
            $app['session']->authenticate(true);

            return $app->redirect($app['url_generator']->generate('index'));
            }
        }

    return $app['twig']->render('login.html.twig', array('error_msg' => 'No such user or password'));
});

Extending the model

Let's take a more complexe case, imagine users are identified with their login and their department info plus we want to be able to store key value informations (needs hstore extension and according pomm converter registered to the database, see Pomm's documentation):

=$ CREATE TABLE my_app.app_user (
      dept char(3),
      extra_infos hstore,
      primary key(login,dept)
   )
   INHERITS (pomm_guard.pomm_user);
CREATE TABLE
=$ \d my_user
            Table "my_app.my_user"
   Column   |        Type         | Modifiers
------------+---------------------+-----------
 login      | character varying   | not null
 password   | character varying   | not null
 groups     | character varying[] |
 dept       | character(3)        | not null
 extra_info | hstore              |
Indexes:
    "my_user_pkey" PRIMARY KEY, btree (login, dept)
Check constraints:
    "check_groups" CHECK (pomm_guard.groups_exist(groups))
Inherits: pomm_guard.pomm_user

When generating the model files, you must specifically rebuild the base file for your users and/or groups to tell Pomm that parents namespace cannot be guessed from the database information:

$scan = new Pomm\Tools\CreateBaseMapTool(array(
    'schema' => 'my_app',
    'table'  => 'my_user',
    'database' => $app['pomm']->getDatabase(),
    'prefix_dir' => PROJECT_DIR.'/sources/model',
    'parent_namespace' => '\GHub\Silex\PommGuard\Model'
    ));

By default, entity classes extend Pomm\Object\BaseObject, change MyUser class to extend \GHub\Silex\PommGuard\Model\PommUser and you're done.

If you also overload the PommGroup class, you have to tell PommUser of it in the initialize method of your PommUserMap class:

public function initialize()
{
    parent::initialize();

    $this->group_map = $this->connection
        ->getMapFor('\Your\Schema\GroupEntity');
}

Of course, this group map class has to extend the base class provided by PommGuard as it expects to have at least the given structure.