Author: Cyril Ogana
Posted on: 2015-07-20
Package: PHP User Credentials
Categories: Password Management And Policy
Read this article to learn how to use this class as a plugin to improve the security capabilities of a PHP framework, in this example the OpenBiz-Cubi Framework.
Contents
Introduction
Plugins for PHP Frameworks
Openbiz-Cubi Overview
Developing the UserCredential Plugin for Openbiz-Cubi
Pseudo Code of the Login Process
Conclusion
Introduction
The PHP User Credentials package is able to integrate password entropy and policy checks into an existing application to make it more robust in terms of the security of the user passwords and policies that make it harder to exploit eventual security breaches.
This article demonstrates how to integrate the PHP User Credentials package as a plug in into an existing framework, in this example the Openbiz-Cubi framework, a data driven framework.
Plugins for PHP Frameworks
PHP Web Frameworks have helped propelled the language into an Enterprise ready state, providing several components as well as reducing the time to build applications by providing reusable components.
Most PHP frameworks support integration of other third party libraries or packages through a plugin architecture.
Based on the definition from the Wikipedia, in computing, a plug-in (or extension or add-on) is a software component that adds a specific feature to an existing application. When an application supports plug-ins, it enables customization.
As per the above definition, plugins have varying names. In Symfony 1 they are known as plugins and in version 2 as bundles, in Yii as extensions, in Openbiz-Cubi they are known as services.
Openbiz-Cubi Overview
Openbiz-Cubi is a PHP based rapid application development (RAD) platform. The goal of Openbiz-Cubi is to provide a set of commonly used modules and tools at a platform level to boost productivity. Openbiz-Cubi is metadata driven, and is heavily inspired by JSF and Struts Java Frameworks.
The platform is a full stack framework. Once installed it already provides an administration module complete with UI for various tasks like creating users.
The framework supports module development via generation of component metadata from XML files, thus with minimal programming effort.
Like in Java, modules are referred to by the notation package.subpackage.ClassName, which refer to the file in the location package/subpackage/ClassName.php located in modules folder of the framework.
Likewise, service.usercredentialService refers to a file usercredentialService.php located in the service directory of the framework.
Component view and model files are located inside a module folder. Therefore. user.view.LoginView refers to a metadata configuration for a view called LoginView.xml file located in the module folder user/view; and system.do.UserDO refers to a model file UserDO.xml in the system/do directory. Note that do stands for data object.
A detailed description of the framework is beyond the scope of this article. However, the main principles of the framework are metadata oriented development, MVC architecture, Object-Relational Mapping and plugin services.
Notice that the framework has claimed ability to work with different database vendors, but will need some work on your end if you are not using MySQL, MariaDB or Percona. I recommend using MySQL or its variants.
For a description of how to install the framework, visit the documentation Wiki page.
Developing the UserCredential Plugin for Openbiz-Cubi
a) Default Openbiz-Cubi Authentication Flow Of Control
The first step is to quickly explain the default authentication flow of control in Openbiz-Cubi.
Logging In
Action | View | Form | Model | Class | Service (Plugin) |
User visits application home screen, whose relative path is /user/login | user. view. LoginView | user. form. LoginForm | user. form. LoginForm | ||
user.form.LoginForm class calls the authService, which is the default authentication plugin for the Openbiz-Cubi framework | user. form. LoginForm | authService | |||
authService performs username against password check using the method checkPassword() on the User table set the in system.do.UserDO metadata file and redirects to default home page or issues login error as an inline div on the login screen | system. do. UserDO | authService |
Session Authentication
View | Form | Model | Class | Service (Plugin) | |
User visits a resource that requires authentication | |||||
The application Controller (BizController) calls the method BizSystem::getUserProfile. | BizController BizSystem | profileService | |||
profileService authenticates user using getProfileByCookie() method. This in turn calls authenticateUserByCookies() method provided by authService; provided the session is not exceeded the time limit set by the session service | services. profileService | authService | |||
If session is not expired and the cookies checked out, profileService builds array of the users profile, and returns it to BizSystem else redirects to the view for session expired (or could not access page using credentials) | profileService |
Password Change
View | Form | Model | Class | Service (Plugin) | |
User visits the location /myaccount/ reset_password in the Openbiz-Cubi application | myaccount. view. ResetPasswordView | myaccount. form. ResetPasswordForm | system. do. UserDO | myaccount. form. ResetPasswordForm | |
User Edits and Saves new password through the ResetPassword() method provided by the class myaccount. form. Reset PasswordForm. A notification of success in edit is provided to user via an inline div element on the Password change form | myaccount. view. ResetPasswordView | myaccount. form. ResetPasswordForm | system. do. UserDO | myaccount. form. ResetPasswordForm |
b. Customized Openbiz-Cubi Authentication Flow Of Control
In this step, we explain how to plug in the support for UserCredential service in the various processes described in section a.
Schema Changes
To support the new UserCredential Service, you will need some additional tables. Below is the SQL to create these tables, which have been tested in MySQL and its variants.
SQL | DESCRIPTION |
CREATE TABLE `usercredential_userstatetype` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `description` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `description_UNIQUE` (`description`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 ; INSERT INTO usercredential_userstatetype VALUES (1,USERCREDENTIAL ACCOUNTSTATE LOGGEDOUT); INSERT INTO usercredential_userstatetype VALUES (2,USERCREDENTIAL ACCOUNTSTATE LOGGEDIN); INSERT INTO usercredential_userstatetype VALUES (3,USERCREDENTIAL ACCOUNTSTATE LOCKED1); INSERT INTO usercredential_userstatetype VALUES (4,USERCREDENTIAL ACCOUNTSTATE LOCKED2); INSERT INTO usercredential_userstatetype VALUES (5,USERCREDENTIAL ACCOUNTSTATE RESET); INSERT INTO usercredential_userstatetype VALUES (6,USERCREDENTIAL ACCOUNTSTATE SUSPENDED); INSERT INTO usercredential_userstatetype VALUES (7,USERCREDENTIAL ACCOUNTSTATE WEAKPASSWD); | This table stores the states a user account can have in the system. The primary keys of the values in this table are symmetrical to the USERCREDENTIAL_USERSATE* named constants that come in the file config/NamedConstant.php which is in the PHPUserCredential package. As such, it is imperative to run the INSERT SQL on the left as well. NB: For additional security, you may give the database user under which the application runs only read only rights at table level for this particular table, as well as usercredential_passwordhistorytype table. |
CREATE TABLE `usercredential_passwordhistorytype` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `description` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `description_UNIQUE` (`description`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1 INSERT INTO usercredential_passwordhistorytype VALUES(1,ADMIN RESET); INSERT INTO usercredential_passwordhistorytype VALUES(2,USER CHANGE); | This table stores values that represent the circumstance under which a user password may change. The two entries ADMIN RESET and USER CHANGE should be included by default, so you should run the INSERT queries as well |
CREATE TABLE `usercredential_passwordhistory` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, `history_type_id` int(11) unsigned NOT NULL, `password` varchar(60) NOT NULL, `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `usercredentialpasswordhistory_UNIQUE_1` (`user_id`,`timestamp`), KEY `passwordhistorytype_passwordhistory_fk1_idx` (`history_type_id`), CONSTRAINT `passwordhistorytype_passwordhistory_fk1` FOREIGN KEY (`history_type_id`) REFERENCES `usercredential_passwordhistorytype` (`id`) ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | Stores the historical password hashes against which password history changes are checked. It will need to be maintained, as may grow large over time. I shall discuss this in another post |
CREATE TABLE `usercredential_userstate` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `user_id` int(10) unsigned NOT NULL, `userstatetype_id` int(11) unsigned NOT NULL, `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, `description` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `usercredentialuserstate_UNIQUE_1` (`user_id`,`timestamp`,`userstatetype_id`), KEY `userstatetype_usercredential_fk1_idx` (`userstatetype_id`), CONSTRAINT `userstatetype_usercredential_fk1` FOREIGN KEY (`userstatetype_id`) REFERENCES `usercredential_userstatetype` (`id`) ON UPDATE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=latin1 | Each change in state in a user table is recorded in this table. The user state table is the most frequently accessed by applications in terms of reads. It also is the table that grows fastest, so maintenance is required. We will cover that in another topic going forward. |
ALTER TABLE user ADD COLUMN `lastpasswordchange` DATETIME NULL DEFAULT NULL, ADD COLUMN `failedlogincount` TINYINT(1) NULL DEFAULT NULL, ADD COLUMN `lastfailedlogin` DATETIME NULL DEFAULT NULL | The Openbiz-Cubi framework comes with a user table which is created when you install the framework. The ALTER script adds three columns that enable us plug in the account policies to support PHPUserCredential service. If you are not comfortable altering the default table, you may have this in a separate table and maintain a 1 - 1 relationship to the user table. |
Logging In Via PHP UserCredentials
View | Form | Model | Class | Service (Plugin) | |
User visits application home screen, whose relative path is /user/login | user. view. LoginView | user. form. LoginForm | user. form. LoginForm | ||
LoginForm class calls the authService, which is the default authentication plugin for the Openbiz-Cubi framework | user. form. LoginForm | authService | |||
We have modified the authService method checkPassword() to authenticate by using the usercredentialService method called authenticate(). This enables us to use the bcrypt authentication enforced by usercredentialService instead Openbiz-Cubi which uses unsalted one way hashes | user credential Service | ||||
If Login is not successful, we update user account state by calling usercredentialService method updateAccountState() and increment failed login attempts. Else it continues by verifying user policy as described below. | user credential Service | ||||
usercredentialService will then build the userprofile. This is enabled by its method getUserProfile() which returns a data array as required by the PHPUserCredential class. Example array is below: array( | system.do.UserDO system. do. Usercredential passwordhistory typeDO system. do. Usercredential passwordhistory system. do. Usercredential userstatetypeDO system. do. Usercredential userstateDO | user credential Service | |||
usercredentialService performs performs password policy check e.g. for password expiry, account is locked, account is suspended, password is weak. It throws an Exception if there is an error, else returns flow of control to the LoginForm | user credential Service | ||||
LoginForm, if there are no exceptions, redirects to default page If Exception was thrown, LoginForm should have try / catch block for UserCredentialException to handle such exceptions. | system.do.UserDO | user. form. LoginForm |
Session Authentication
Action | View | Form | Model | Class | Service (Plugin) |
User visits a resource that requires authentication | |||||
Controller calls the method BizSystem::getUserProfile. | BizController | profileService | |||
profileService authenticates user using Cookies method supported by authService, provided the session is not exceeded the limit set by the session service | services.profileService | authService | |||
Our authService is customized to delegate authentication to the usercredentialService | usercredentialService | ||||
UserCredentialService builds userProfile | usercredentialService | ||||
usercredentialService verifies the policy and if okay returns flow of control. If there is a policy issue, it throws a UserCredentialException | usercredentialService | ||||
If session is not expired and the cookies checked out, profileService builds array of the users profile, and returns it to BizSystem else redirects to the views for session expired (or could not access page using credentials) If an Exception is thrown, BizSystem needs to handle UserCredentialException to take the appropriate action. | BizSystem | profileService |
Password Change
View | Form | Model | Class | Service (Plugin) | |
User visits the location /myaccount /reset_password in the Openbiz-Cubi application | myaccount. view. ResetPasswordView | myaccount. form. ResetPasswordForm | system. do. UserDO | myaccount. form. Reset Password Form | |
User Edits and Saves new password. | myaccount. view. ResetPasswordView | myaccount. form. ResetPasswordForm | system. do. UserDO | myaccount. form. Reset Password Form | |
The Password Reset form class delegates the entropy and policy verification to UserCredentialService | user credential Service | ||||
On Success, user receives a popup. If failed, the Reset Class has to handle a User CredentialException in order to properly handle this e.g. entropy is not sufficient, password history error |
Pseudo Code of the Login Process
- User visits Login View
- Login View loads Login Form
- User fills in username and password then Submits Form
- Log in Form submits data to the Log in Manager class (AJAX, POST)
- Log in Manager authenticates username / password via the PHP UserCredential package
- If authentication details have been verified then
Build the userProfile array as required by PHP UserCredential
Get the users Account state from userProfile array
If users account state is \USERCREDENTIAL_ACCOUNTSTATE_SUSPENDED then
Load notification on user interface that account is suspended
Exit
ElseIf users account state is \USERCREDENTIAL_ACCOUNTSTATE_LOCKED2 then
Load notification on user interface that account is locked out indefinitely for multiple illegal attempts
Exit
ElseIf users account state is \USERCREDENTIAL_ACCOUNTSTATE_LOCKED1
Get the boolean value of true if required seconds elapsed since account was locked has been attained, else false (provided by the processTempLockout() method in our Openbiz-Cubi plugin
If seconds elapsed check returned true
Continue Processing
Else
Load notification on user interface to user indicating the account has been temporarily locked out for illegal attempts and they should attempt to log in again in a few minutes
Exit
ElseIf users account state is \USERCREDENTIAL_ACCOUNTSTATE_RESET or \USERCREDENTIAL_ACCOUNTSTATE_LOGGEDIN or \USERCREDENTIAL_ACCOUNTSTATE_LOGGEDOUT
Try
Validate Entropy of the Password (Provided by validateEntropy() method of the Openbiz-Cubi plugin)
Catch UserCredentialException
User interface should notify and redirect user to change the password as is not of required entropy
Try
Validate Policy of the User Profile (Provided by validatePolicy() method of Openbiz-Cubi plugin). Based on the state, it will validate for password expiry
Catch UserCredentialException
User interface should notify and redirect user to change the password due to policy restriction on the profile
Update user account state to \USERCREDENTIAL_ACCOUNTSTATA_LOGGEDIN (provided by updateAccountState() method of the Openbiz-Cubi plugin
Direct User to Home Screen
- Else if Authentication Failed
Build the userProfile array as required by PHP UserCredential
Get the users Account state from userProfile array
Increment The Uses Account Failed Login state by 1 via updateUser() method of usercredentialService plugin
Try
Validate user Profile against policy (provided by ValidatePolicy method)
Catch
If Exception Code is \USERCREDENTIAL_ACCOUNTOPOLICY_ATTEMPTLIMIT1 Then
Update users account state to \USERCREDENTIAL_ACCOUNTSTATE_LOCKED1, i.e they are temporarily locked
ElseIf Exception Code is \USERCREDENTIAL_ACCOUNTPOLICY_ATTEMPTLIMIT2 Then
Update users account to \USERCREDENTIAL_ACCOUNTSTATE_LOCKED2, i.e they are indefinetly locked
Else
Update users account to \USERCREDENTIAL_ACCOUNTSTATE_LOGGEDOUT
User Interface should inform user of the appropriate state of their account. If it is not yet one of the locked out states, it should invite them to attempt to log in again.
Exit
Conclusion
When in college, I remember one of my classmates telling me The Login process is long. Not so nuch for the user, but he was studying Oracle at the moment and noted the authentication and authorization steps involved are quite many on the back end.
The UserCredential Service provides capability to implement the authentication policies in an environment where passwords are primary method used used to authenticate, strengthening the process by providing policies for the password management.
To use the package, you generally require ability to build the user profile, as well as to handle the exception codes returned by the package if it raises one. In a future post, we will implement the package as a Yii extension.
The Openbiz-Cubi authentication Service plugin can be downloaded here as part of the PHP User Credentials package. It may also provide an illustration of how you can implement a plugin for your application.
If you liked this article or have questions regarding using this package with the Openbiz-Cubi or other frameworks, post a comment here.
You need to be a registered user or login to post a comment
1,338,680 PHP developers registered to the PHP Classes site.
Be One of Us!
Login Immediately with your account on:
Comments:
No comments were submitted yet.