PHP
Article

User Authentication in Symfony2 with UserApp.io

By Daniel Sipos

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.

Tablet Security theme

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 it
  • refreshUser() – which gets called on each authenticated request
  • supportsClass() – 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:

  1. we create a token with the submitted credentials (createToken())
  2. we try to authenticate the credentials in this token and throw an authentication exception if we fail
  3. we create a new token containing the user object and some other information if authentication is successful
  4. 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:

  1. If the token has expired in the meantime in UserApp.io, we get an INVALID_CREDENTIALS valued exception so by throwing a Symfony AuthenticationException we log the user out in Symfony as well.
  2. 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. The locked 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:

  1. 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.

  2. 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.

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
OmarEGeek

thanks
This also works with soap webservice ?
where they would have to change to work with a soap webservice ?
regards grin

upchuk

Hey,

I'm not sure. You can consult their website to see what can and cannot be done with their service. Personally, REST is all I care about smile

OmarEGeek

I have to do something similar but instead of using userapp.io have to
use soap webservice that changes would have to do in this instance?
regards

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in PHP, once a week, for free.