webCOMAND

Login Framework

io_comand_login

A framework for website and app user login features, including:

  • Authentication - Validate user logins based on credentials, such as username and password.
  • Forgot Password - Retrieve login credentials, such as a username or password.
  • Security Questions - Manage security questions and answers for alternative user validation.
  • Reset Credentials - Trigger and process user login credential resets.
  • Change Credentials - Process manual user-requested login credential updates.
  • Login Security Log - Tracks login successes and failures used by security features and for an audit trail.
  • Lock/Unlock - Lock out users based on failed login attempts and handle the unlock process.
  • Sessions - Maintain user login status and user-specific website and app state.
To learn how Users, User Roles, Authorizations and login features all work together, see Users in the Developer Guide.

Usage

Define how your website and/or app users will login by creating the following objects.

A basic best practice package should be made available for easy import and modification of each of the following to help developers get started.

Create Package

Use an existing package for your website or app or create a new one.  The package will contain all of your login implementation objects and code.  For example, for a website create a package with the namespace "com_example_www".

User Content Type

First, decide how you want to store and manage users.  There are generally three options:

  • Use User - Use the built-in webCOMAND User content type if it already provides everything you need to store and manage for your website or app users.  This will be the easiest way to get up and running, and you will automatically get all of the features and functionality afforded to webCOMAND Users.  You can also use the same User record to provide access to webCOMAND, webCOMAND Apps and your website and app.
  • Extend User - If you want to be able to define additional website or app-specific user options and features that you are not able to sufficiently implement with webCOMAND User Roles, Authorizations and Privileges, then you can extend the built-in webCOMAND User content type to get the best of both worlds.  You will need to define your own User content type that extends User, but then you will still automatically inherit all of the features and functionality afforded to webCOMAND Users, but can add your own fields.
  • Create a Custom User - If you aren't interested in inheriting any of the built-in webCOMAND User features and functionality, you can create your own unique user content type with only the fields, features and functionality you want.  In this situation, simply create a content type that extends Object, Content or any other content type you would like.

User Model (PHP Class)

If you created a custom user in the previous step, create a PHP model to interface between your user content type and the login API.  See User Model alternative example.

Models (Objects)

Next create each of the following model objects:

  • User Password Model - Defines Password Requirements with a Regular Expression.
  • User Model - Defines where to find the PHP User model class, which you may have created in the previous step.  If not, you leave all fields blank and the default \io_comand_login\models\user\UserModel.php class will be used.
  • Cookies Session Model -
  • Reset Code Model -
  • Security Question Model -

Controllers

  • Change Controller -
  • Login Controller -
  • Reset Controller -
  • Unlock Controller -

Login Policy

Once all of the Models and Controllers have been defined, a Login Policy is used to pull it all together.

Login Code

The Login Policy that configures the login API features can now be easily loaded from code.

 

This:

<?php
require_once('/var/www/webcomand/comand.php');

class example_login {

    const LOGIN_POLICY_OID = '123';
    const RESET_PASSWORD_URL = 'https://presidentsdemo.com/login/reset';
    const RESET_LOCK_URL = 'https://demo.webcomand.com/com_webcomand/components/login/reset_lock_link';

    private static $login = NULL;

    public function __construct(array $options = []) {
        $repo = $$options['repo'] ?? \comand::repo();
        $policy = $$this->repo->get(self::LOGIN_POLICY_OID);
        $this->login = new \io_comand_login\login($$policy, $$repo);
        $this->login->set('ResetLockLink', self::RESET_LOCK_URL);
        return self::$login_object;
    }

    private static $user=false;//false is we dont' know if we're logged in, null is we're not

    public static function login(string $username,string $password){
        try{
            $login=self::get_login_object();
            $login->set('account', $username);
            $login->set('password', $password);
            self::$user=$login->login->login();
            return self::$user;
        }catch(\io_comand_login\exception $e){
            switch($e->getCode()){
                case \io_comand_login\exception::LOGIN_ERROR_CONFIGURATION:
                case \io_comand_login\exception::LOGIN_ERROR_SYSTEMLOCKED:
                    throw $e;
                    break;
                default:
                    throw new \io_comand_login\exception("No user found for given credentials.", \io_comand_login\exception::LOGIN_ERROR_NOUSER);
            }
        }
    }

    public static function logout(){
        $login=self::get_login_object();
        $login->login->logout();
        self::$user=null;
    }

    public static function is_logged_in(){
        if(self::$user===false){
            $login=self::get_login_object();
            self::$user=$login->login->is_logged_in();
        }
        return self::$user;
    }

    public static function has_authorization(int $authorization_type){
        if($authorization_type===32683)//if public
            return true;
        $user=self::is_logged_in();
        if(!$user)
            return null;
        if(!$user->authorized_for($authorization_type))
            return false;
        return $user;
    }

    public static function change_password(string $old_password,string $new_password,string $confirm_password){
        $login=self::get_login_object();
        $login->set('account', self::$user->OID);
        $login->set('old_password', $old_password);
        $login->set('new_password', $new_password);
        $login->set('confirm_password', $confirm_password);
        $login->change->change_password();
        self::$user->SecurePassword=true;
        self::$user->approve();
    }
    
    public static function change_security_question(string $password,string $question,string $answer){
        $login=self::get_login_object();
        $login->set('account', self::$user->OID);
        $question_model=$login->get_model('securityquestion');
        $credentials_model=$login->get_model('credentials');
        if(self::$user->OID!=$credentials_model->challenge($password))
            throw new exception("User mismatch or cannot discover user from old password", \io_comand_login\exception::LOGIN_ERROR_BADUSERINPUT);
        $question_model->invalidate_all_questions(self::$user->OID);
        $question_model->add_question(self::$user->OID,$question,$answer);
    }

    public static function forgot_password(string $email){
        try{
            $login=self::get_login_object();
            $login->set('account', $email);
            $login->set('link', 'RESET_PASSWORD_URL');
            $login->reset->forgot_password();
        }catch(\io_comand_login\exception $e){
            switch($e->getCode()){
                case \io_comand_login\exception::LOGIN_ERROR_CONFIGURATION:
                case \io_comand_login\exception::LOGIN_ERROR_SYSTEMLOCKED:
                    throw $e;
            }
        }
    }
    
    public static function verify_reset_code(string $code){
        try{
            $login=self::get_login_object();
            $login->set('account', $code);
            if($login->reset->verify_code($code))
                return [$login->get('question_id'),$login->get('question')];
        }catch(\io_comand_login\exception $e){
            switch($e->getCode()){
                case \io_comand_login\exception::LOGIN_ERROR_CONFIGURATION:
                case \io_comand_login\exception::LOGIN_ERROR_SYSTEMLOCKED:
                    throw $e;
            }
        }
        return null;
    }

    public static function reset_password(string $code,int $question_id,string $answer,string $new_password,string $confirm_password){
        $login=self::get_login_object();
        $login->set('account', $code);
        $login->set('question_id', $question_id);
        $login->set('answer', $answer);
        $login->set('new_password', $new_password);
        $login->set('confirm_password', $confirm_password);
        $login->reset->reset_password($code);
    }
}