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.
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.
To authenticate UserApp.io users with our Symfony app, we’ll create a few classes:
Once we create these classes, we will declare some of them as services and use them within the Symfony security system.
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:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppAuthenticator.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\HttpFoundation\Request</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\TokenInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span> </span><span><span>class UserAppAuthenticator implements SimpleFormAuthenticatorInterface </span></span><span><span>{ </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) </span></span><span> <span>{ </span></span><span> </span><span> <span>try { </span></span><span> <span>$login = $this->userAppClient->user->login(array( </span></span><span> <span>"login" => $token->getUsername(), </span></span><span> <span>"password" => $token->getCredentials(), </span></span><span> <span>) </span></span><span> <span>); </span></span><span> </span><span> <span>// Load user from provider based on id </span></span><span> <span>$user = $userProvider->loadUserByLoginInfo($login); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') { </span></span><span> <span>throw new AuthenticationException('Invalid username or password'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') { </span></span><span> <span>throw new AuthenticationException('Invalid app ID'); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> <span>return new UserAppToken( </span></span><span> <span>$user, </span></span><span> <span>$user->getToken(), </span></span><span> <span>$providerKey, </span></span><span> <span>$user->getRoles() </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsToken(TokenInterface $token, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return $token instanceof UserAppToken </span></span><span> <span>&& $token->getProviderKey() === $providerKey; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function createToken(Request $request, $username, $password, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return new UserAppToken($username, $password, $providerKey); </span></span><span> <span>} </span></span><span><span>}</span></span>
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.
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppToken.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\UsernamePasswordToken</span>; </span></span><span> </span><span><span>class UserAppToken extends UsernamePasswordToken { </span></span><span> </span><span><span>}</span></span>
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).
Next, let’s see how the authenticator works with the user provider, so it’s time to create the latter as well:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppProvider.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\UsernameNotFoundException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\UnsupportedUserException</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span><span>use AppBundle<span>\Security\Exception\NoUserRoleException</span>; </span></span><span><span>use AppBundle<span>\Security\UserAppUser</span>; </span></span><span> </span><span><span>class UserAppProvider implements UserProviderInterface </span></span><span><span>{ </span></span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function loadUserByUsername($username) </span></span><span> <span>{ </span></span><span> <span>// Empty for now </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function refreshUser(UserInterface $user) </span></span><span> <span>{ </span></span><span> <span>if (!$user instanceof UserAppUser) { </span></span><span> <span>throw new UnsupportedUserException( </span></span><span> <span>sprintf('Instances of "%s" are not supported.', get_class($user)) </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>try { </span></span><span> <span>$api = $this->userAppClient; </span></span><span> <span>$api->setOption('token', $user->getToken()); </span></span><span> <span>$api->token->heartbeat(); </span></span><span> <span>$user->unlock(); </span></span><span> <span>} </span></span><span> <span>catch (ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_CREDENTIALS') { </span></span><span> <span>throw new AuthenticationException('Invalid credentials'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'AUTHORIZATION_USER_LOCKED') { </span></span><span> <span>$user->lock(); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>return $user; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsClass($class) </span></span><span> <span>{ </span></span><span> <span>return $class === 'AppBundle\Security\UserAppUser'; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * </span></span><span><span> * Loads a user from UserApp.io based on a successful login response. </span></span><span><span> * </span></span><span><span> * <span>@param $login </span></span></span><span><span> * <span>@return UserAppUser </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>public function loadUserByLoginInfo($login) { </span></span><span> </span><span> <span>try { </span></span><span> <span>$api = $this->userAppClient; </span></span><span> <span>$api->setOption('token', $login->token); </span></span><span> <span>$users = $api->user->get(); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_USER_ID') { </span></span><span> <span>throw new UsernameNotFoundException(sprintf('User with the id "%s" not found.', $login->user_id)); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>if (!empty($users)) { </span></span><span> <span>return $this->userFromUserApp($users[0], $login->token); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * Creates a UserAppUser from a user response from UserApp.io </span></span><span><span> * </span></span><span><span> * <span>@param $user </span></span></span><span><span> * <span>@param $token </span></span></span><span><span> * <span>@return UserAppUser </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>private function userFromUserApp($user, $token) { </span></span><span> </span><span> <span>$roles = $this->extractRolesFromPermissions($user); </span></span><span> </span><span> <span>$options = array( </span></span><span> <span>'id' => $user->user_id, </span></span><span> <span>'username' => $user->login, </span></span><span> <span>'token' => $token, </span></span><span> <span>'firstName' => $user->first_name, </span></span><span> <span>'lastName' => $user->last_name, </span></span><span> <span>'email' => $user->email, </span></span><span> <span>'roles' => $roles, </span></span><span> <span>'properties' => $user->properties, </span></span><span> <span>'features' => $user->features, </span></span><span> <span>'permissions' => $user->permissions, </span></span><span> <span>'created' => $user->created_at, </span></span><span> <span>'locked' => !empty($user->locks), </span></span><span> <span>'last_logged_in' => $user->last_login_at, </span></span><span> <span>'last_heartbeat' => time(), </span></span><span> <span>); </span></span><span> </span><span> <span>return new UserAppUser($options); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * Extracts the roles from the permissions list of a user </span></span><span><span> * </span></span><span><span> * <span>@param $user </span></span></span><span><span> * <span>@return <span>array</span> </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>private function extractRolesFromPermissions($user) { </span></span><span> <span>$permissions = get_object_vars($user->permissions); </span></span><span> <span>if (empty($permissions)) { </span></span><span> <span>throw new NoUserRoleException('There are no roles set up for your users.'); </span></span><span> <span>} </span></span><span> <span>$roles = array(); </span></span><span> <span>foreach ($permissions as $role => $permission) { </span></span><span> <span>if ($permission->value === TRUE) { </span></span><span> <span>$roles[] = $role; </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>if (empty($roles)) { </span></span><span> <span>throw new NoUserRoleException('This user has no roles enabled.'); </span></span><span> <span>} </span></span><span> </span><span> <span>return $roles; </span></span><span> <span>} </span></span><span><span>}</span></span>
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:
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:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppAuthenticator.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\HttpFoundation\Request</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\TokenInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span> </span><span><span>class UserAppAuthenticator implements SimpleFormAuthenticatorInterface </span></span><span><span>{ </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) </span></span><span> <span>{ </span></span><span> </span><span> <span>try { </span></span><span> <span>$login = $this->userAppClient->user->login(array( </span></span><span> <span>"login" => $token->getUsername(), </span></span><span> <span>"password" => $token->getCredentials(), </span></span><span> <span>) </span></span><span> <span>); </span></span><span> </span><span> <span>// Load user from provider based on id </span></span><span> <span>$user = $userProvider->loadUserByLoginInfo($login); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') { </span></span><span> <span>throw new AuthenticationException('Invalid username or password'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') { </span></span><span> <span>throw new AuthenticationException('Invalid app ID'); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> <span>return new UserAppToken( </span></span><span> <span>$user, </span></span><span> <span>$user->getToken(), </span></span><span> <span>$providerKey, </span></span><span> <span>$user->getRoles() </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsToken(TokenInterface $token, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return $token instanceof UserAppToken </span></span><span> <span>&& $token->getProviderKey() === $providerKey; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function createToken(Request $request, $username, $password, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return new UserAppToken($username, $password, $providerKey); </span></span><span> <span>} </span></span><span><span>}</span></span>
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:
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 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.
Let’s also create the UserAppUser class we’ve been talking about earlier:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppAuthenticator.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\HttpFoundation\Request</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\TokenInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span> </span><span><span>class UserAppAuthenticator implements SimpleFormAuthenticatorInterface </span></span><span><span>{ </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) </span></span><span> <span>{ </span></span><span> </span><span> <span>try { </span></span><span> <span>$login = $this->userAppClient->user->login(array( </span></span><span> <span>"login" => $token->getUsername(), </span></span><span> <span>"password" => $token->getCredentials(), </span></span><span> <span>) </span></span><span> <span>); </span></span><span> </span><span> <span>// Load user from provider based on id </span></span><span> <span>$user = $userProvider->loadUserByLoginInfo($login); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') { </span></span><span> <span>throw new AuthenticationException('Invalid username or password'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') { </span></span><span> <span>throw new AuthenticationException('Invalid app ID'); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> <span>return new UserAppToken( </span></span><span> <span>$user, </span></span><span> <span>$user->getToken(), </span></span><span> <span>$providerKey, </span></span><span> <span>$user->getRoles() </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsToken(TokenInterface $token, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return $token instanceof UserAppToken </span></span><span> <span>&& $token->getProviderKey() === $providerKey; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function createToken(Request $request, $username, $password, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return new UserAppToken($username, $password, $providerKey); </span></span><span> <span>} </span></span><span><span>}</span></span>
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.
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.
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppToken.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\UsernamePasswordToken</span>; </span></span><span> </span><span><span>class UserAppToken extends UsernamePasswordToken { </span></span><span> </span><span><span>}</span></span>
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.
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:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppProvider.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\UsernameNotFoundException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\UnsupportedUserException</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span><span>use AppBundle<span>\Security\Exception\NoUserRoleException</span>; </span></span><span><span>use AppBundle<span>\Security\UserAppUser</span>; </span></span><span> </span><span><span>class UserAppProvider implements UserProviderInterface </span></span><span><span>{ </span></span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function loadUserByUsername($username) </span></span><span> <span>{ </span></span><span> <span>// Empty for now </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function refreshUser(UserInterface $user) </span></span><span> <span>{ </span></span><span> <span>if (!$user instanceof UserAppUser) { </span></span><span> <span>throw new UnsupportedUserException( </span></span><span> <span>sprintf('Instances of "%s" are not supported.', get_class($user)) </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>try { </span></span><span> <span>$api = $this->userAppClient; </span></span><span> <span>$api->setOption('token', $user->getToken()); </span></span><span> <span>$api->token->heartbeat(); </span></span><span> <span>$user->unlock(); </span></span><span> <span>} </span></span><span> <span>catch (ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_CREDENTIALS') { </span></span><span> <span>throw new AuthenticationException('Invalid credentials'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'AUTHORIZATION_USER_LOCKED') { </span></span><span> <span>$user->lock(); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>return $user; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsClass($class) </span></span><span> <span>{ </span></span><span> <span>return $class === 'AppBundle\Security\UserAppUser'; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * </span></span><span><span> * Loads a user from UserApp.io based on a successful login response. </span></span><span><span> * </span></span><span><span> * <span>@param $login </span></span></span><span><span> * <span>@return UserAppUser </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>public function loadUserByLoginInfo($login) { </span></span><span> </span><span> <span>try { </span></span><span> <span>$api = $this->userAppClient; </span></span><span> <span>$api->setOption('token', $login->token); </span></span><span> <span>$users = $api->user->get(); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_USER_ID') { </span></span><span> <span>throw new UsernameNotFoundException(sprintf('User with the id "%s" not found.', $login->user_id)); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>if (!empty($users)) { </span></span><span> <span>return $this->userFromUserApp($users[0], $login->token); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * Creates a UserAppUser from a user response from UserApp.io </span></span><span><span> * </span></span><span><span> * <span>@param $user </span></span></span><span><span> * <span>@param $token </span></span></span><span><span> * <span>@return UserAppUser </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>private function userFromUserApp($user, $token) { </span></span><span> </span><span> <span>$roles = $this->extractRolesFromPermissions($user); </span></span><span> </span><span> <span>$options = array( </span></span><span> <span>'id' => $user->user_id, </span></span><span> <span>'username' => $user->login, </span></span><span> <span>'token' => $token, </span></span><span> <span>'firstName' => $user->first_name, </span></span><span> <span>'lastName' => $user->last_name, </span></span><span> <span>'email' => $user->email, </span></span><span> <span>'roles' => $roles, </span></span><span> <span>'properties' => $user->properties, </span></span><span> <span>'features' => $user->features, </span></span><span> <span>'permissions' => $user->permissions, </span></span><span> <span>'created' => $user->created_at, </span></span><span> <span>'locked' => !empty($user->locks), </span></span><span> <span>'last_logged_in' => $user->last_login_at, </span></span><span> <span>'last_heartbeat' => time(), </span></span><span> <span>); </span></span><span> </span><span> <span>return new UserAppUser($options); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * Extracts the roles from the permissions list of a user </span></span><span><span> * </span></span><span><span> * <span>@param $user </span></span></span><span><span> * <span>@return <span>array</span> </span></span></span><span><span> * <span>@throws NoUserRoleException </span></span></span><span><span> */ </span></span><span> <span>private function extractRolesFromPermissions($user) { </span></span><span> <span>$permissions = get_object_vars($user->permissions); </span></span><span> <span>if (empty($permissions)) { </span></span><span> <span>throw new NoUserRoleException('There are no roles set up for your users.'); </span></span><span> <span>} </span></span><span> <span>$roles = array(); </span></span><span> <span>foreach ($permissions as $role => $permission) { </span></span><span> <span>if ($permission->value === TRUE) { </span></span><span> <span>$roles[] = $role; </span></span><span> <span>} </span></span><span> <span>} </span></span><span> </span><span> <span>if (empty($roles)) { </span></span><span> <span>throw new NoUserRoleException('This user has no roles enabled.'); </span></span><span> <span>} </span></span><span> </span><span> <span>return $roles; </span></span><span> <span>} </span></span><span><span>}</span></span>
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:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\Exception\NoUserRoleException.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security\Exception</span>; </span></span><span> </span><span><span>class NoUserRoleException extends <span>\Exception</span> { </span></span><span> </span><span><span>}</span></span>
Here we specify that our application has also this user provider so it can use it.
Under the firewall key, add the following:
<span><span><?php </span></span><span> </span><span><span>/** </span></span><span><span> * <span>@file AppBundle\Security\UserAppAuthenticator.php </span></span></span><span><span> */ </span></span><span> </span><span><span>namespace AppBundle<span>\Security</span>; </span></span><span> </span><span><span>use Symfony<span>\Component\HttpFoundation\Request</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\SimpleFormAuthenticatorInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Authentication\Token\TokenInterface</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\Exception\AuthenticationException</span>; </span></span><span><span>use Symfony<span>\Component\Security\Core\User\UserProviderInterface</span>; </span></span><span><span>use UserApp<span>\API</span> as UserApp; </span></span><span><span>use UserApp<span>\Exceptions\ServiceException</span>; </span></span><span> </span><span><span>class UserAppAuthenticator implements SimpleFormAuthenticatorInterface </span></span><span><span>{ </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>@var UserApp </span></span></span><span><span> */ </span></span><span> <span>private $userAppClient; </span></span><span> </span><span> <span>public function __construct(UserApp $userAppClient) { </span></span><span> <span>$this->userAppClient = $userAppClient; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) </span></span><span> <span>{ </span></span><span> </span><span> <span>try { </span></span><span> <span>$login = $this->userAppClient->user->login(array( </span></span><span> <span>"login" => $token->getUsername(), </span></span><span> <span>"password" => $token->getCredentials(), </span></span><span> <span>) </span></span><span> <span>); </span></span><span> </span><span> <span>// Load user from provider based on id </span></span><span> <span>$user = $userProvider->loadUserByLoginInfo($login); </span></span><span> <span>} catch(ServiceException $exception) { </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_LOGIN' || $exception->getErrorCode() == 'INVALID_ARGUMENT_PASSWORD') { </span></span><span> <span>throw new AuthenticationException('Invalid username or password'); </span></span><span> <span>} </span></span><span> <span>if ($exception->getErrorCode() == 'INVALID_ARGUMENT_APP_ID') { </span></span><span> <span>throw new AuthenticationException('Invalid app ID'); </span></span><span> <span>} </span></span><span> <span>} </span></span><span> <span>return new UserAppToken( </span></span><span> <span>$user, </span></span><span> <span>$user->getToken(), </span></span><span> <span>$providerKey, </span></span><span> <span>$user->getRoles() </span></span><span> <span>); </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function supportsToken(TokenInterface $token, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return $token instanceof UserAppToken </span></span><span> <span>&& $token->getProviderKey() === $providerKey; </span></span><span> <span>} </span></span><span> </span><span> <span>/** </span></span><span><span> * <span>{@inheritdoc} </span></span></span><span><span> */ </span></span><span> <span>public function createToken(Request $request, $username, $password, $providerKey) </span></span><span> <span>{ </span></span><span> <span>return new UserAppToken($username, $password, $providerKey); </span></span><span> <span>} </span></span><span><span>}</span></span>
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
The above is the detailed content of User Authentication in Symfony2 with UserApp.io. For more information, please follow other related articles on the PHP Chinese website!