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)

The built-in webCOMAND Login Policy Models are typically used.  However, you can extend or replace them if needed.  The following models can be reused or copied and customized.

  • 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 - Implements user session functionality, including options to control:
    • Inactive Seconds - The number of seconds of inactivity before a user's session should expire.  0 indicates unlimited session inactivity expiration.
    • Allow Remember Me - Enable 'remember me' functionality, which will allow cross-browser sessions with an expiration based on Inactive Seconds.
    • Cookie Settings - Cookie Name, Path, Domain, Secure, HTTP Only, Strict Mode
    • Session Setting - Path Depth determines if session files will be automatically distributed according to the first (or optionally second) session ID character, to increase performance by reducing the number of files in a single directory.  A value above 2 is not recommended, as this can require a very large (64n) number of inodes to store these files.  0 means no subdirectories.
  • Reset Code Model - Implements reset code generation and verification.
  • Security Question Model - Implements security questions and verification.

Controllers (Objects)

The built-in webCOMAND Login Policy Controllers are typically used.  However, you can extend or replace them if needed, including:

  • Change Controller - Implements authentication change functionality, such as password change.
  • Login Controller - Implements the user authentication functionality.
  • Reset Controller - Implements the reset authentication functionality, such as reset password.
  • Unlock Controller - Implements the functionality used to lock and unlock users based on login attempts and other activity.

Login Policy

Once you have determined and/or implement which Models and Controllers to use, a Login Policy is used to pull it all together.

Login Example Code

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

<?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);
    }
}