Edit on GitHub

Authenticate against Pimcore Objects

As Symfony's security component is quite complex, Pimcore provides base implementations to facilitate integrating the security configuration with users stored as Pimcore objects.

As example, assume we have a user object which is defined in a AppBundle\Model\DataObject\User class and stores its password in a field named password (field type Password). The password field is configured to use the password_hash algorithm which is the standard way to handle passwords in PHP these days (internally it uses bcrypt). The class definition looks like this (you can find a working example in the demo-basic install profile):

AppBundle\Model\DataObject\User

As a user object needs to implement the UserInterface provided by Symfony, we override the generated class and implement the remaining methods which are not implemented by field getters:

<?php
// src/AppBundle/Model/DataObject/User.php

namespace AppBundle\Model\DataObject;

use Pimcore\Model\DataObject\ClassDefinition\Data\Password;
use Pimcore\Model\DataObject\User as BaseUser;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * Our custom user class implementing Symfony's UserInterface.
 */
class User extends BaseUser implements UserInterface
{
    /**
     * @inheritDoc
     */
    public function getSalt()
    {
        // user has no salt as we use password_hash
        // which handles the salt by itself
        return null;
    }

    /**
     * Trigger the hash calculation to remove the plain text password from the instance. This
     * is necessary to make sure no plain text passwords are serialized.
     *
     * @inheritDoc
     */
    public function eraseCredentials()
    {
        /** @var Password $field */
        $field = $this->getClass()->getFieldDefinition('password');
        $field->getDataForResource($this->getPassword(), $this);
    }
}

Next, we configure Pimcore to use our overridden class:

# src/AppBundle/Resources/config/pimcore/config.yml
pimcore:
    models:
        class_overrides:
            'Pimcore\Model\DataObject\User': 'AppBundle\Model\DataObject\User'

Loading users with a User Provider

A user provider is responsible for finding matching user objects for a given username. Pimcore ships an ObjectUserProvider which loads users from a defined class type and searches the username for a configured property. In our case, we want to load users from the AppBundle\Model\DataObject\User and query the username field. To be able to use our user class in the security configuration, we define a user provider service which is configured to load our user implementation (make sure your bundle is able to load service definitions, see Loading Service Definitions):

# src/AppBundle/Resources/config/services.yml
services:
    # The user provider loads users by Username.
    # Pimcore provides a simple ObjectUserProvider which is able to load users from a specified class by a configured
    # field. The website_demo.security.user_provider will load users from the AppBundle\Model\DataObject\User by looking at
    # their username field.
    website_demo.security.user_provider:
        class: Pimcore\Security\User\ObjectUserProvider
        arguments: ['AppBundle\Model\DataObject\User', 'username']

We'll use this service later in our security configuration to tell the firewall where to load its users from. For details have a look at ObjectUserProvider which is basically calling User::getByUsername($username, 1) internally. If you have more complex use cases you can extend the ObjectUserProvider or ship your completely custom implementation.

For more information see How to Create a custom User Provider on the Symfony docs.

Password encoding

The standard approach of hashing and verifying a user's password in Symfony is to delegate the logic to to a PasswordEncoderInterface which is responsible for calculating and verifying password hashes. As Pimcore's Password field definition already provides this logic, the password encoder needs to be configured to delegate the logic to the user object.

Symfony builds and caches one encoder instance per user type (class). To be able to delegate the calculation to the user object it is necessary to build an encoder instance which is scoped to the user object and can access the user's properties at runtime. Pimcore adds this as additional layer of configuration which allows to specify an encoder factory per user type which in turn can decide if it needs to build dedicated instances of encoders per user.

To be able to integrate our user object, we need 2 integration points:

  • A PasswordFieldEncoder which has access to the user instance and delegates calculation and verification of the password hash to the password field definition. The encoder needs to be configured with the name of the field it should operate on (password in our case).
  • A UserAwareEncoderFactory which builds a dedicated instance of a PasswordFieldEncoder per user object.

To achieve this, we define a factory service which builds PasswordFieldEncoder instances as specified above:

# The encoder factory is responsible for verifying the password hash for a given user. As we need some special
# handling to be able to work with the password field, we use the UserAwareEncoderFactory to buiild a dedicated
# encoder per user. This service is configured in pimcore.security.encoder_factories to handle our user model.
services:
    website_demo.security.password_encoder_factory:
        class: Pimcore\Security\Encoder\Factory\UserAwareEncoderFactory
        arguments:
            - Pimcore\Security\Encoder\PasswordFieldEncoder
            - ['password']

Now, instead of configuring the encoder in security.encoders as it is the standard Symfony way, configure your encoder factory service instead in pimcore.security.encoder_factories. This is just an additional way of building encoders - if you don't need any user specific handling, just stick to the standard Symfony way.

pimcore:
    security:
        # the encoder factory as defined in services.yml
        encoder_factories:
            AppBundle\Model\DataObject\User: website_demo.security.password_encoder_factory

When an encoder is loaded for a AppBundle\Model\DataObject\User object, the UserAwareEncoderFactory will build a dedicated instance of PasswordFieldEncoder instead of always returning the same instance for all users.

Configuring the firewall

As all our needed services are in place, we can start to use them from the firewall configuration. As an example, let's configure a simple firewall which authenticates via HTTP basic auth. Our final configuration looks like the following:

pimcore:
    security:
        # the encoder factory as defined in services.yml
        encoder_factories:
            AppBundle\Model\DataObject\User: website_demo.security.password_encoder_factory

security:
    providers:
        # the user provider as defined in services.yml
        demo_cms_provider:
            id: website_demo.security.user_provider

    firewalls:
        # demo_cms firewall is valid for the whole site
        demo_cms_fw:
            # the provider defined above
            provider: demo_cms_provider
            http_basic: ~

This should get you started with a custom authentication system based on Pimcore objects. For further information see: