How to Do User Authentication With the Symfony Security Component

In this article, you’ll learn how to set up user authentication in PHP using the Symfony Security component. As well as authentication, I’ll show you how to use its role-based authorization, which you can extend according to your needs.

How to Do User Authentication With the Symfony Security Component

The Symfony Security Component

The Symfony Security Component allows you to set up security features like authentication, role-based authorization, CSRF tokens and more very easily. In fact, it’s further divided into four sub-components which you can choose from according to your needs.

The Security component has the following sub-components:

  • symfony/security-core
  • symfony/security-http
  • symfony/security-csrf
  • symfony/security-acl

In this article, we are going to explore the authentication feature provided by the symfony/security-core component.

As usual, we’ll start with the installation and configuration instructions, and then we’ll explore a few real-world examples to demonstrate the key concepts.

Installation and Configuration

In this section, we are going to install the Symfony Security component. I assume that you have already installed Composer on your system—we’ll need it to install the Security component available at Packagist.

So go ahead and install the Security component using the following command.

$composer require symfony/security

We are going to load users from the MySQL database in our example, so we’ll also need a database abstraction layer. Let’s install one of the most popular database abstraction layers: Doctrine DBAL.

$composer require doctrine/dbal

That should have created the composer.json file, which should look like this:

{
    "require": {
        "symfony/security": "^4.1",
        "doctrine/dbal": "^2.7"
    }
}

Let’s modify the composer.json file to look like the following one.

{
    "require": {
        "symfony/security": "^4.1",
        "doctrine/dbal": "^2.7"
    },
    "autoload": {
         "psr-4": {
             "Sfauth\": "src"
         },
         "classmap": ["src"]
    }
}

As we have added a new classmap entry, let’s go ahead and update the composer autoloader by running the following command.

$composer dump -o

Now, you can use the Sfauth namespace to autoload classes under the src directory.

So that’s the installation part, but how are you supposed to use it? In fact, it’s just a matter of including the autoload.php file created by Composer in your application, as shown in the following snippet.

<?php
require_once './vendor/autoload.php';

// application code
?>

A Real-World Example

Firstly, let’s go through the usual authentication flow provided by the Symfony Security component.

  • The first thing is to retrieve the user credentials and create an unauthenticated token.
  • Next, we’ll pass an unauthenticated token to the authentication manager for validation.
  • The authentication manager may contain different authentication providers, and one of them will be used to authenticate the current user request. The logic of how the user is authenticated is defined in the authentication provider.
  • The authentication provider contacts the user provider to retrieve the user. It’s the responsibility of the user provider to load users from the respective back-end.
  • The user provider tries to load the user using the credentials provided by the authentication provider. In most cases, the user provider returns the user object that implements the UserInterface interface.
  • If the user is found, the authentication provider returns an unauthenticated token, and you can store this token for the subsequent requests.

In our example, we are going to match the user credentials against the MySQL database, thus we’ll need to create the database user provider. We’ll also create the database authentication provider that handles the authentication logic. And finally, we’ll create the User class, which implements the UserInterface interface.

The User Class

In this section, we’ll create the User class which represents the user entity in the authentication process.

Go ahead and create the src/User/User.php file with the following contents.

<?php
namespace SfauthUser;

use SymfonyComponentSecurityCoreUserUserInterface;

class User implements UserInterface
{
    private $username;
    private $password;
    private $roles;

    public function __construct(string $username, string $password, string $roles)
    {
        if (empty($username))
        {
            throw new InvalidArgumentException('No username provided.');
        }

        $this->username = $username;
        $this->password = $password;
        $this->roles = $roles;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getRoles()
    {
        return explode(",", $this->roles);
    }

    public function getSalt()
    {
        return '';
    }

    public function eraseCredentials() {}
}

The important thing is that the User class must implement the Symfony Security UserInterface interface. Apart from that, there’s nothing out of the ordinary here.

The Database Provider Class

It’s the responsibility of the user provider to load users from the back-end. In this section, we’ll create the database user provider, which loads the user from the MySQL database.

Let’s create the src/User/DatabaseUserProvider.php file with the following contents.

<?php
namespace SfauthUser;

use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreExceptionUsernameNotFoundException;
use SymfonyComponentSecurityCoreExceptionUnsupportedUserException;
use DoctrineDBALConnection;
use SfauthUserUser;

class DatabaseUserProvider implements UserProviderInterface
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function loadUserByUsername($username)
    {
        return $this->getUser($username);
    }

    private function getUser($username)
    {
        $sql = "SELECT * FROM sf_users WHERE username = :name";
        $stmt = $this->connection->prepare($sql);
        $stmt->bindValue("name", $username);
        $stmt->execute();
        $row = $stmt->fetch();

        if (!$row['username'])
        {
            $exception = new UsernameNotFoundException(sprintf('Username "%s" not found in the database.', $row['username']));
            $exception->setUsername($username);
            throw $exception;
        }
        else
        {
            return new User($row['username'], $row['password'], $row['roles']);
        }
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User)
        {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->getUser($user->getUsername());
    }

    public function supportsClass($class)
    {
        return 'SfauthUserUser' === $class;
    }
}

The user provider must implement the UserProviderInterface interface. We are using the doctrine DBAL to perform the database-related operations. As we have implemented the UserProviderInterface interface, we must implement the loadUserByUsername, refreshUser, and supportsClass methods.

The loadUserByUsername method should load the user by the username, and that’s done in the getUser method. If the user is found, we return the corresponding SfauthUserUser object, which implements the UserInterface interface.

On the other hand, the refreshUser method refreshes the supplied User object by fetching the latest information from the database.

And finally, the supportsClass method checks if the DatabaseUserProvider provider supports the supplied user class.

The Database Authentication Provider Class

Finally, we need to implement the user authentication provider, which defines the authentication logic—how a user is authenticated. In our case, we need to match the user credentials against the MySQL database, and thus we need to define the authentication logic accordingly.

Go ahead and create the src/User/DatabaseAuthenticationProvider.php file with the following contents.

<?php
namespace SfauthUser;

use SymfonyComponentSecurityCoreAuthenticationProviderUserAuthenticationProvider;
use SymfonyComponentSecurityCoreUserUserProviderInterface;
use SymfonyComponentSecurityCoreUserUserCheckerInterface;
use SymfonyComponentSecurityCoreExceptionUsernameNotFoundException;
use SymfonyComponentSecurityCoreExceptionAuthenticationServiceException;
use SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;

class DatabaseAuthenticationProvider extends UserAuthenticationProvider
{
    private $userProvider;

    public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, string $providerKey, bool $hideUserNotFoundExceptions = true)
    {
        parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);

        $this->userProvider = $userProvider;
    }

    protected function retrieveUser($username, UsernamePasswordToken $token)
    {
        $user = $token->getUser();

        if ($user instanceof UserInterface)
        {
            return $user;
        }

        try {
            $user = $this->userProvider->loadUserByUsername($username);

            if (!$user instanceof UserInterface)
            {
                throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
            }

            return $user;
        } catch (UsernameNotFoundException $e) {
            $e->setUsername($username);

            throw $e;
        } catch (Exception $e) {
            $e = new AuthenticationServiceException($e->getMessage(), 0, $e);
            $e->setToken($token);
            throw $e;
        }
    }

    protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
    {
        $currentUser = $token->getUser();
        
        if ($currentUser instanceof UserInterface)
        {
            if ($currentUser->getPassword() !== $user->getPassword())
            {
                throw new AuthenticationException('Credentials were changed from another session.');
            }
        }
        else
        {
            $password = $token->getCredentials();

            if (empty($password))
            {
                throw new AuthenticationException('Password can not be empty.');
            }

            if ($user->getPassword() != md5($password))
            {
                throw new AuthenticationException('Password is invalid.');
            }
        }
    }
}

The DatabaseAuthenticationProvider authentication provider extends the UserAuthenticationProvider abstract class. Hence, we need to implement the retrieveUser and checkAuthentication abstract methods.

The job of the retrieveUser method is to load the user from the corresponding user provider. In our case, it will use the DatabaseUserProvider user provider to load the user from the MySQL database.

On the other hand, the checkAuthentication method performs the necessary checks in order to authenticate the current user. Please note that I’ve used the MD5 method for password encryption. Of course, you should use more secure encryption methods to store user passwords.

How It Works Altogether

So far, we have created all the necessary elements for authentication. In this section, we’ll see how to put it all together to set up the authentication functionality.

Go ahead and create the db_auth.php file and populate it with the following contents.

<?php
require_once './vendor/autoload.php';

use SfauthUserDatabaseUserProvider;
use SymfonyComponentSecurityCoreUserUserChecker;
use SfauthUserDatabaseAuthenticationProvider;
use SymfonyComponentSecurityCoreAuthenticationAuthenticationProviderManager;
use SymfonyComponentSecurityCoreAuthenticationTokenUsernamePasswordToken;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;

// init doctrine db connection
$doctrineConnection = DoctrineDBALDriverManager::getConnection(
  array('url' => 'mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}'), new DoctrineDBALConfiguration()
);

// init our custom db user provider
$userProvider = new DatabaseUserProvider($doctrineConnection);

// we'll use default UserChecker, it's used to check additional checks like account lock/expired etc.
// you can implement your own by implementing UserCheckerInterface interface
$userChecker = new UserChecker();

// init our custom db authentication provider
$dbProvider = new DatabaseAuthenticationProvider(
    $userProvider,
    $userChecker,
    'frontend'
);

// init authentication provider manager
$authenticationManager = new AuthenticationProviderManager(array($dbProvider));

try {
    // init un/pw, usually you'll get these from the $_POST variable, submitted by the end user
    $username = 'admin';
    $password = 'admin';

    // get unauthenticated token
    $unauthenticatedToken = new UsernamePasswordToken(
        $username,
        $password,
        'frontend'
    );

    // authenticate user & get authenticated token
    $authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);

    // we have got the authenticated token (user is logged in now), it can be stored in a session for later use
    echo $authenticatedToken;
    echo "n";
} catch (AuthenticationException $e) {
    echo $e->getMessage();
    echo "n";
}

Recall the authentication flow which was discussed in the beginning of this article—the above code reflects that sequence.

The first thing was to retrieve the user credentials and create an unauthenticated token.

$unauthenticatedToken = new UsernamePasswordToken(
    $username,
    $password,
    'frontend'
);

Next, we have passed that token to the authentication manager for validation.

// authenticate user & get authenticated token
$authenticatedToken = $authenticationManager->authenticate($unauthenticatedToken);

When the authenticate method is called, a lot of things are happening behind the scenes.

Firstly, the authentication manager selects an appropriate authentication provider. In our case, it’s the DatabaseAuthenticationProvider authentication provider, which will be selected for authentication.

Next, it retrieves the user by the username from the DatabaseUserProvider user provider. Finally, the checkAuthentication method performs the necessary checks to authenticate the current user request.

Should you wish to test the db_auth.php script, you’ll need to create the sf_users table in your MySQL database.

CREATE TABLE `sf_users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) NOT NULL,
  `password` varchar(255) NOT NULL,
  `roles` enum('registered','moderator','admin') DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;

INSERT INTO `sf_users` VALUES (1,'admin','21232f297a57a5a743894a0e4a801fc3','admin');

Go ahead and run the db_auth.php script to see how it goes. Upon successful completion, you should receive an authenticated token, as shown in the following snippet.

$php db_auth.php
UsernamePasswordToken(user="admin", authenticated=true, roles="admin")

Once the user is authenticated, you can store the authenticated token in the session for the subsequent requests.

And with that, we’ve completed our simple authentication demo!

Conclusion

Today, we looked at the Symfony Security component, which allows you to integrate security features in your PHP applications. Specifically, we discussed the authentication feature provided by the symfony/security-core sub-component, and I showed you an example of how this functionality can be implemented in your own app.

Feel free to post your thoughts using the feed below!