UserApp.io is a handy user management tool and API. It provides a web interface to deal with user accounts (and the many features this involves) and an API to hook them into your own web application. The purpose of this service is to make it easier and safer to manage user authentication by not having to worry about that on your own server.
It has SDKs and various wrappers for many programming languages and frameworks and the price is affordable. Yes, it comes with a price but you can get started freely with quite a lot of things to play around with. I recommend checking out their features page to get more information. Also, it’s very easy to create an account and experiment with creating users, adding properties to their profiles, etc, so I recommend you check that out as well if you haven’t already.
In this article, we are going to look at how we can implement a Symfony2 authentication mechanism that leverages UserApp.io. The code we write can also be found in this small library I created (currently in dev) that you can try out. To install it in your Symfony app, just follow the instructions on GitHub.
Dependecies
In order to communicate with the UserApp.io service, we will make use of their PHP library. Make sure you require this in your Symfony application’s composer.json file as instructed on their GitHub page.
The authentication classes
To authenticate UserApp.io users with our Symfony app, we’ll create a few classes:
- A form authenticator class used to perform the authentication with the UserApp.io API
- A custom User class used to represent our users with information gathered from the API
- A user provider class used to retrieve users and transform them into objects of our User class
- A Token class used to represent the Symfony authentication token
- A logout handler class that takes care of logging out from the UserApp.io service.
- A simple exception class that we can throw if the UserApp.io users don’t have any permissions set (that we will convert to Symfony roles)
Once we create these classes, we will declare some of them as services and use them within the Symfony security system.
Form authenticator
First, we will create the most important class, the form authenticator (inside a Security/
folder of our best practice named AppBundle
). Here is the code, I will explain it afterwards:
<?php
/**
* @file AppBundle\Security\UserAppAuthenticator.php
*/
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use UserApp\API as UserApp;
use UserApp\Exceptions\ServiceException;
class UserAppAuthenticator implements SimpleFormAuthenticatorInterface
{
/**
* @var UserApp
*/
private $userAppClient;
public function __construct(UserApp $userAppClient) {
$this->userAppClient = $userAppClient;
}
/**
* {@inheritdoc}
*/
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
try {
$login = $this->userAppClient->user->login(array(
"login" => $token->getUsername(),
"password" => $token->getCredentials(),
)
);
// Load user from provider based on id
$user = $userProvider->loadUserByLoginInfo($login);
} catch(ServiceException $exception) {
if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') {
throw new AuthenticationException('Invalid username or password');
}
if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') {
throw new AuthenticationException('Invalid app ID');
}
}
return new UserAppToken(
$user,
$user->getToken(),
$providerKey,
$user->getRoles()
);
}
/**
* {@inheritdoc}
*/
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof UserAppToken
&& $token->getProviderKey() === $providerKey;
}
/**
* {@inheritdoc}
*/
public function createToken(Request $request, $username, $password, $providerKey)
{
return new UserAppToken($username, $password, $providerKey);
}
}
As you can see, we are implementing the SimpleFormAuthenticatorInterface
and consequently have 3 methods and a constructor. The latter takes a dependency as the instantiated UserApp.io client (passed using the service container, but more on this in a minute).
This class is used by Symfony when a user tries to login and authenticate with the application. The first thing that happens is that createToken()
is called. This method needs to return an authentication token which combines the submitted username and password. In our case, it will be an instance of the UserAppToken
class we will define in a moment.
Then the supportToken()
method is called to check if this class does support the token returned by createToken()
. Here we just make sure we return true for our token type.
Finally, authenticateToken()
gets called and attempts to check whether the credentials in the token are valid. In here, and using the UserApp.io PHP library, we try to log in or throw a Symfony authentication exception if this fails. If the authentication is successful though, the responsible user provider is used to build up our user object, before creating and returning another token object based on the latter.
We will write our user provider right after we quickly create the simple UserAppToken
class.
Token class
<?php
/**
* @file AppBundle\Security\UserAppToken.php
*/
namespace AppBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
class UserAppToken extends UsernamePasswordToken {
}
As you can see, this is just an extension of the UsernamePasswordToken
class for the sake of naming being more accurate (since we are storing a token instead of a password).
User provider
Next, let’s see how the authenticator works with the user provider, so it’s time to create the latter as well:
<?php
/**
* @file AppBundle\Security\UserAppProvider.php
*/
namespace AppBundle\Security;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use UserApp\API as UserApp;
use UserApp\Exceptions\ServiceException;
use AppBundle\Security\Exception\NoUserRoleException;
use AppBundle\Security\UserAppUser;
class UserAppProvider implements UserProviderInterface
{
/**
* @var UserApp
*/
private $userAppClient;
public function __construct(UserApp $userAppClient) {
$this->userAppClient = $userAppClient;
}
/**
* {@inheritdoc}
*/
public function loadUserByUsername($username)
{
// Empty for now
}
/**
* {@inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof UserAppUser) {
throw new UnsupportedUserException(
sprintf('Instances of "%s" are not supported.', get_class($user))
);
}
try {
$api = $this->userAppClient;
$api->setOption('token', $user->getToken());
$api->token->heartbeat();
$user->unlock();
}
catch (ServiceException $exception) {
if ($exception->getErrorCode() == 'INVALID_CREDENTIALS') {
throw new AuthenticationException('Invalid credentials');
}
if ($exception->getErrorCode() == 'AUTHORIZATION_USER_LOCKED') {
$user->lock();
}
}
return $user;
}
/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'AppBundle\Security\UserAppUser';
}
/**
*
* Loads a user from UserApp.io based on a successful login response.
*
* @param $login
* @return UserAppUser
* @throws NoUserRoleException
*/
public function loadUserByLoginInfo($login) {
try {
$api = $this->userAppClient;
$api->setOption('token', $login->token);
$users = $api->user->get();
} catch(ServiceException $exception) {
if ($exception->getErrorCode() == 'INVALID_ARGUMENT_USER_ID') {
throw new UsernameNotFoundException(sprintf('User with the id "%s" not found.', $login->user_id));
}
}
if (!empty($users)) {
return $this->userFromUserApp($users[0], $login->token);
}
}
/**
* Creates a UserAppUser from a user response from UserApp.io
*
* @param $user
* @param $token
* @return UserAppUser
* @throws NoUserRoleException
*/
private function userFromUserApp($user, $token) {
$roles = $this->extractRolesFromPermissions($user);
$options = array(
'id' => $user->user_id,
'username' => $user->login,
'token' => $token,
'firstName' => $user->first_name,
'lastName' => $user->last_name,
'email' => $user->email,
'roles' => $roles,
'properties' => $user->properties,
'features' => $user->features,
'permissions' => $user->permissions,
'created' => $user->created_at,
'locked' => !empty($user->locks),
'last_logged_in' => $user->last_login_at,
'last_heartbeat' => time(),
);
return new UserAppUser($options);
}
/**
* Extracts the roles from the permissions list of a user
*
* @param $user
* @return array
* @throws NoUserRoleException
*/
private function extractRolesFromPermissions($user) {
$permissions = get_object_vars($user->permissions);
if (empty($permissions)) {
throw new NoUserRoleException('There are no roles set up for your users.');
}
$roles = array();
foreach ($permissions as $role => $permission) {
if ($permission->value === TRUE) {
$roles[] = $role;
}
}
if (empty($roles)) {
throw new NoUserRoleException('This user has no roles enabled.');
}
return $roles;
}
}
Similar to the form authenticator class, we inject the UserApp.io client into this class using dependency injection and we implement the UserProviderInterface
. The latter requires we have 3 methods:
loadUserByUsername()
– which we leave empty for now as we don’t need itrefreshUser()
– which gets called on each authenticated requestsupportsClass()
– which determines whether this user provider works with our (yet to be created) User class.
Let’s turn back a second to our authenticator class and see what exactly happens when authentication with UserApp.io is successful: we call the custom loadUserByLoginInfo()
method on the user provider class which takes a successful login result object from the API and uses its authentication token to request back from the API the logged-in user object. The result gets wrapped into our own local UserAppUser
class via the userFromUserApp()
and extractRolesFromPermissions()
helper methods. The latter is my own implementation of a way to translate the concept of permissions
in UserApp.io into roles
in Symfony. And we throw our own NoUserRoleException
if the UserApp.io is not set up with permissions for the users. So make sure that your users in UserApp.io have permissions that you want to map to roles in Symfony.
The exception class is a simple extension from the default PHP \Exception
:
<?php
/**
* @file AppBundle\Security\Exception\NoUserRoleException.php
*/
namespace AppBundle\Security\Exception;
class NoUserRoleException extends \Exception {
}
Back to our authenticator again, we see that if the authentication with UserApp.io is successful, a UserAppUser
classed object is built by the user provider containing all the necessary info on the user. Having this object, we need to add it to a new instance of the UserAppToken
class and return it.
So basically this happens from the moment a user tries to log in:
- we create a token with the submitted credentials (
createToken()
) - we try to authenticate the credentials in this token and throw an authentication exception if we fail
- we create a new token containing the user object and some other information if authentication is successful
- we return this token which Symfony will then use to store the user in the session.
The refreshUser()
method on the user provider is also very important. This method is responsible for retrieving a new instance of the currently logged in user on each authenticated page refresh. So whenever the authenticated user goes to any of the pages inside the firewall, this method gets triggered. The point is to hydrate the user object with any changes in the storage that might have happened in the meantime.
Obviously we need to keep API calls to a minimum but this is a good opportunity to increase the authentication time of UserApp.io by sending a heartbeat request. By default (but configurable), each authenticated user token is valid for 60 minutes but by sending a heartbeat request, this gets extended by 20 minutes.
This is a great place to perform also two other functions:
- If the token has expired in the meantime in UserApp.io, we get an
INVALID_CREDENTIALS
valued exception so by throwing a SymfonyAuthenticationException
we log the user out in Symfony as well. - Although heartbeat requests are made to be as cheap as possible (which means no real user data is retrieved), the user
locked
status does get transmitted back in the form of an exception. So we can take this opportunity and mark our User object locked as well. Thelocked
status can then be used in the application, for example, by checking against it and denying access to various parts if the user is locked.
If you want, you can make an API request and update the user object with data from UserApp.io here as well but I find it doesn’t make much sense for most use cases. Data can be updated when the user logs out and back in the next time. But depending on the needs, this can be easily done here. Although keep in mind the performance implications and the cost of many API calls to UserApp.io.
And basically that is the crux of our authentication logic.
The User class
Let’s also create the UserAppUser
class we’ve been talking about earlier:
<?php
/**
* @file AppBundle\Security\UserAppUser.php
*/
namespace AppBundle\Security;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\User\UserInterface;
class UserAppUser implements UserInterface {
private $id;
private $username;
private $token;
private $firstName;
private $lastName;
private $email;
private $roles;
private $properties;
private $features;
private $permissions;
private $created;
private $locked;
public function __construct($options)
{
$resolver = new OptionsResolver();
$this->configureOptions($resolver);
$params = $resolver->resolve($options);
foreach ($params as $property => $value) {
$this->{$property} = $value;
}
}
/**
* Configures the class options
*
* @param $resolver OptionsResolver
*/
private function configureOptions($resolver)
{
$resolver->setDefaults(array(
'id' => NULL,
'username' => NULL,
'token' => NULL,
'firstName' => NULL,
'lastName' => NULL,
'email' => NULL,
'roles' => array(),
'properties' => array(),
'features' => array(),
'permissions' => array(),
'created' => NULL,
'locked' => NULL,
'last_logged_in' => NULL,
'last_heartbeat' => NULL,
));
$resolver->setRequired(array('id', 'username'));
}
/**
* {@inheritdoc}
*/
public function getRoles()
{
return $this->roles;
}
/**
* {@inheritdoc}
*/
public function getToken()
{
return $this->token;
}
/**
* {@inheritdoc}
*/
public function getSalt()
{
}
/**
* {@inheritdoc}
*/
public function getUsername()
{
return $this->username;
}
/**
* {@inheritdoc}
*/
public function eraseCredentials()
{
}
/**
* {@inheritdoc}
*/
public function getPassword() {
}
/**
* @return mixed
*/
public function getId() {
return $this->id;
}
/**
* @return array
*/
public function getProperties() {
return $this->properties;
}
/**
* @return mixed
*/
public function isLocked() {
return $this->locked;
}
/**
* Locks the user
*/
public function lock() {
$this->locked = true;
}
/**
* Unlocks the user
*/
public function unlock() {
$this->locked = false;
}
/**
* @return mixed
*/
public function getFirstName() {
return $this->firstName;
}
/**
* @return mixed
*/
public function getLastName() {
return $this->lastName;
}
/**
* @return mixed
*/
public function getEmail() {
return $this->email;
}
/**
* @return mixed
*/
public function getFeatures() {
return $this->features;
}
/**
* @return mixed
*/
public function getCreated() {
return $this->created;
}
/**
* @return mixed
*/
public function getPermissions() {
return $this->permissions;
}
}
Nothing particular here, we are just mapping some data from UserApp.io and implementing some of the methods required by the interface. Additionally we added the locked/unlocked
flagger.
Logout
The last class we need to create is the one that deals with logging the user out from UserApp.io when they log out of Symfony.
<?php
/**
* @file AppBundle\Security\UserAppLogout.php
*/
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Logout\LogoutHandlerInterface;
use UserApp\API as UserApp;
use UserApp\Exceptions\ServiceException;
class UserAppLogout implements LogoutHandlerInterface {
/**
* @var UserApp
*/
private $userAppClient;
public function __construct(UserApp $userAppClient) {
$this->userAppClient = $userAppClient;
}
/**
* {@inheritdoc}
*/
public function logout(Request $request, Response $response, TokenInterface $token) {
$api = $this->userAppClient;
$user = $token->getUser();
$api->setOption('token', $user->getToken());
try {
$api->user->logout();
}
catch (ServiceException $exception) {
// Empty for now, error probably caused by user not being authenticated which means
// user is logged out already.
}
}
}
Here again we inject the UserApp.io PHP client and since we implement the LogoutHandlerInterface
we need to have a logout()
method. All we do in it is log the user out from UserApp.io if they’re still logged in.
Wiring everything up
Now that we have our classes, it’s time to declare them as services and use them in our authentication system. Here are our YML based service declarations:
user_app_client:
class: UserApp\API
arguments: ["%userapp_id%"]
user_app_authenticator:
class: AppBundle\Security\UserAppAuthenticator
arguments: ["@user_app_client"]
user_app_provider:
class: AppBundle\Security\UserAppProvider
arguments: ["@user_app_client"]
user_app_logout:
class: AppBundle\Security\UserAppLogout
arguments: ["@user_app_client"]
The first one is the UserApp.io PHP library to which we pass in our application ID in the form of a reference to a parameter. You will need to have a parameter called userapp_id
with your UserApp.io App ID.
The other three are the form authenticator, user provider and logout classes we wrote earlier. And as you remember, each accepts one parameter in their constructor in the form of the UserApp.io client defined as the first service.
Next, it’s time to use these services in our security system, so edit the security.yml
file and do the following:
Under the
providers
key, add the following:user_app: id: user_app_provider
Here we specify that our application has also this user provider so it can use it.
Under the
firewall
key, add the following:
secured_area:
pattern: ^/secured/
simple_form:
authenticator: user_app_authenticator
check_path: security_check
login_path: login
logout:
path: logout
handlers: [user_app_logout]
target: _home
anonymous: ~
What happens here is that we define a simple secure area which uses the simple_form
type of authentication with our authenticator. Under the logout
key we are adding a handler to be called (our UserAppLogout
class defined as a service). The rest is regular Symfony security setup so make sure you do have a login form being shown on the login
route, etc. Check out the documentation on this for more information.
And that’s all. By using the simple_form
authentication with our custom form authenticator and user provider (along with an optional logout handler), we’ve implemented our own UserApp.io based Symfony authentication mechanism.
Conclusion
In this article, we’ve seen how to implement a custom Symfony form authentication using the UserApp.io service and API as a user provider. We’ve gone through quite a lot of code which meant a very brief explanation of the code itself. Rather, I tried to explain the process of authentication with Symfony by building a custom solution that takes into account the way we can interact with UserApp.io.
If you followed along and implemented this method inside your bundle and want to use it like this, go ahead. You also have the option of using the library I created which has a very quick and easy setup described on the GitHub page. I recommend the latter because I plan on developing and maintaining it so you can always get an updated version if any bugs are removed or features introduced (hope not the other way around).
If you would like to contribute to it, you’re very welcome. I also appreciate letting me know if you find any problems or think there are better ways to accomplish similar goals.
Frequently Asked Questions (FAQs) on User Authentication with Symfony2 and UserApp.io
How can I integrate UserApp.io with Symfony2 for user authentication?
Integrating UserApp.io with Symfony2 for user authentication involves a few steps. First, you need to install the UserApp library using Composer. Then, you need to configure the UserApp service in your Symfony2 project. This involves setting up the UserApp API key and configuring the UserApp service in the services.yml file. After that, you can use the UserApp service in your controllers to authenticate users.
What are the benefits of using UserApp.io for user authentication in Symfony2?
UserApp.io provides a number of benefits for user authentication in Symfony2. It simplifies the process of user management by providing a ready-made solution for user authentication, registration, password reset, and more. It also provides a secure and scalable solution for user authentication, which can be very beneficial for large-scale applications.
How can I handle user roles and permissions with UserApp.io in Symfony2?
UserApp.io provides a feature called “User Roles” that allows you to manage user roles and permissions. You can define different roles and assign them to users. Then, you can check the user’s role in your Symfony2 controllers to control access to different parts of your application.
How can I handle user registration with UserApp.io in Symfony2?
UserApp.io provides a feature called “User Registration” that allows you to handle user registration in your Symfony2 application. You can use the UserApp service in your controllers to register new users. The UserApp service will handle the registration process, including validating the user’s email and password, and creating a new user account.
How can I handle password reset with UserApp.io in Symfony2?
UserApp.io provides a feature called “Password Reset” that allows you to handle password reset in your Symfony2 application. You can use the UserApp service in your controllers to reset a user’s password. The UserApp service will handle the password reset process, including sending a password reset email to the user.
How can I handle user authentication errors with UserApp.io in Symfony2?
UserApp.io provides a feature called “Error Handling” that allows you to handle user authentication errors in your Symfony2 application. You can use the UserApp service in your controllers to catch and handle authentication errors. The UserApp service will provide detailed error messages that you can use to debug and fix authentication issues.
How can I customize the user authentication process with UserApp.io in Symfony2?
UserApp.io provides a number of customization options for the user authentication process. You can customize the login form, the registration form, the password reset form, and more. You can also customize the user authentication process by adding custom fields to the user profile, or by implementing custom authentication logic.
How can I secure my Symfony2 application with UserApp.io?
UserApp.io provides a number of security features that can help you secure your Symfony2 application. It provides secure user authentication, secure password storage, and secure user management. It also provides features like two-factor authentication and IP whitelisting that can further enhance the security of your application.
How can I migrate my existing user data to UserApp.io in Symfony2?
UserApp.io provides a feature called “Data Migration” that allows you to migrate your existing user data to UserApp.io. You can use the UserApp API to import your existing user data into UserApp.io. The UserApp API provides a number of endpoints that you can use to import user data, including user profiles, user roles, and user permissions.
How can I troubleshoot issues with UserApp.io in Symfony2?
UserApp.io provides a number of troubleshooting tools that can help you troubleshoot issues with UserApp.io in Symfony2. It provides detailed error messages, logging, and debugging tools. You can also use the UserApp API to troubleshoot issues with the UserApp service. The UserApp API provides a number of endpoints that you can use to debug and troubleshoot issues with the UserApp service.
Daniel Sipos is a Drupal developer who lives in Brussels, Belgium. He works professionally with Drupal but likes to use other PHP frameworks and technologies as well. He runs webomelette.com, a Drupal blog where he writes articles and tutorials about Drupal development, theming and site building.