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.
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 Sfauth\User; use Symfony\Component\Security\Core\User\UserInterface; 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 Sfauth\User; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Doctrine\DBAL\Connection; use Sfauth\User\User; 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 'Sfauth\User\User' === $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 Sfauth\User\User
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 Sfauth\User; use Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider; use Symfony\Component\Security\Core\User\UserProviderInterface; use Symfony\Component\Security\Core\User\UserCheckerInterface; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Exception\AuthenticationServiceException; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; 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 Sfauth\User\DatabaseUserProvider; use Symfony\Component\Security\Core\User\UserChecker; use Sfauth\User\DatabaseAuthenticationProvider; use Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; // init doctrine db connection $doctrineConnection = \Doctrine\DBAL\DriverManager::getConnection( array('url' => 'mysql://{USERNAME}:{PASSWORD}@{HOSTNAME}/{DATABASE_NAME}'), new \Doctrine\DBAL\Configuration() ); // 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!
No comments:
Post a Comment