7

I used zf2 authentication for authenticate user in my project.I saved Harib in my user table as user name but if i use my user name Harib then its accept or if i use harib then its not accept,i want to remove case sensitivity of user name so both Harib or harib access how i fix this?

Here is my code:

public function loginAction()
{
    $this->layout('layout/login-layout.phtml');
    $login_error = false;
    $loginForm = new LoginForm();
    $form_elements = json_encode($loginForm->form_elements);
    if ($this->request->isPost()) {
        $post = $this->request->getPost();
        $loginForm->setData($post);
        if ($loginForm->isValid()) {
            $hashed_string = '';
            if(
                array_key_exists('hashed_input' , $post) &&
                $post['hashed_input'] != '' &&
                strpos(urldecode($this->params('redirect')) , 'programdetailrequest') !== false
            ) {
                $hashed_string = $post['hashed_input'];
            }
            $data = $loginForm->getData();
            $authService = $this->getServiceLocator()->get('doctrine.authenticationservice.odm_default');
            $adapter = $authService->getAdapter();
            $adapter->setIdentityValue($data['username']);
            $adapter->setCredentialValue(md5($data['password']));
            $authResult = $authService->authenticate();
            if($authResult->isValid()){
                $identity = $authResult->getIdentity();
                if( is_object($identity) && method_exists($identity, 'getData') ){
                    $user_data = $identity->getData();
                    $authService->getStorage()->write($identity);
                    // for remeber checkbox
                    if ($post['rememberme']) {
                        $token = new UserToken();
                        $dm = $this->getServiceLocator()->get('doctrine.documentmanager.odm_default');
                        //if same user already running from other browser then remove previous token.
                        $check_token = $dm->getRepository('Admin\Document\UserToken')->findOneBy(array( "user_id.id" => $user_data['id'] ));
                        if (is_object($check_token) && !is_null($check_token)) {
                            $remove_token = $dm->createQueryBuilder('Admin\Document\UserToken')
                                ->remove()
                                ->field('id')->equals($check_token->id)
                                ->getQuery()->execute();
                        }
                        //create token
                        $user = $dm->getRepository('Admin\Document\User')->findOneBy(array( "id" => $user_data['id'] ));
                        $token->setProperty('user_id', $user);
                        $token->setProperty('dataentered', new \MongoDate());
                        $dm->persist($token);
                        $dm->flush($token);
                        //create cookie
                        if(is_object($token) && property_exists($token, 'id')){
                            $time = time() + (60 * 60 * 24 * 30); // 1 month
                            setcookie('token', $token->getProperty('id'), $time, '/');
                        }
                    }
                    if ($user_data['user_type'] == 'onlinemarketer') {
                        $this->redirect()->toRoute('admin_program_meta');
                    } elseif ($user_data['user_type'] == 'bucharestofficemanager') {
                        $this->redirect()->toRoute('admin_program_detail_request');
                    } else {
                        if ($this->params('redirect') && urldecode($this->params('redirect')) !== '/logout/') {
                            $server_url = $this->getRequest()->getUri()->getScheme() . '://' . $this->getRequest()->getUri()->getHost().urldecode($this->params('redirect') . $hashed_string);
                            return $this->redirect()->toUrl($server_url);
                        }
                        return $this->redirect()->toRoute('admin_index');
                    }
                }
            } else {
                $identity = false;
                $login_error = true;
            }
        }
    }
    return new ViewModel(array(
            'loginForm' => $loginForm,
            'form_elements' =>$form_elements,
            'login_error' => $login_error,
    ));
}

and here is my login form code:

<?php
namespace Admin\Form;

use Zend\Form\Form;
use Zend\Form\Element;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\Factory as InputFactory;

class LoginForm extends Form implements InputFilterAwareInterface
{
protected $inputFilter;
public $form_elements = array(
    array(
        'name' => 'username',
        'attributes' => array(
            'id' => 'username',
            'type'  => 'text',
            'error_msg' => 'Enter Valid Username',
            'data-parsley-required' => 'true',
            'data-parsley-pattern' => '^[a-zA-Z0-9_\.\-]{1,50}$',
            'data-parsley-trigger' => 'change'
        ),
        'options' => array(
            'label' => 'User Name'
        ), 
        'validation' => array(
            'required'=>true,
            'filters'=> array(
                array('name'=>'StripTags'),
                array('name'=>'StringTrim')
            ),
            'validators'=>array(
                array('name'=>'Regex',
                    'options'=> array(
                        'pattern' => '/^[a-z0-9_.-]{1,50}+$/', // contain only a to z 0 to 9 underscore, hypen and space, min 1 max 50
                        'pattern_js' => '^[a-zA-Z0-9_\.\-]{1,50}$' 
                    )
                )
            )
        )
    ),
    array(
        'name' => 'password',
        'attributes' => array(
            'id' => 'password',
            'type'  => 'password',
            'error_msg' => 'Enter Valid Password',
            'data-parsley-required' => 'true',
            'data-parsley-pattern' => '^[a-zA-Z0-9_\.\-]{6,25}$',
            'data-parsley-trigger' => 'change'
        ),
        'options' => array(
            'label' => 'Password'
        ), 
        'validation' => array(
            'required' => true,
            'filters'=> array(
                array('name'=>'StripTags'),
                array('name'=>'StringTrim')
            ),
            'validators'=>array(
                array('name'=>'Regex',
                    'options'=> array(
                        'pattern' => '/^[a-z0-9_.-]{6,25}+$/', // contain only a to z 0 to 9 underscore, hypen and space, min 1 max 50
                        'pattern_js' => '^[a-zA-Z0-9_\.\-]{6,25}$' 
                    )
                )
            )
        )
    ),
    array(
        'name' => 'hashed_input',
        'attributes' => array(
            'type'  => 'hidden',
            'id' => 'hashed_input',
            'value' => ''
        )
    ),
    array(
        'name' => 'rememberme',
        'attributes' => array(
            'value' => 1,
            'id' => 'rememberme',
            'type' => 'Checkbox'
        ),
        'options' => array(
            'label' => 'Remember Me',
            'use_hidden_element' => false,
        )
    ),
    array(
        'name' => 'submit',
        'attributes' => array(
            'type'  => 'submit',
            'value' => 'Log in',
            'id' => 'submitbutton'
        )
    )
);
public function __construct()
{
    parent::__construct('user');
    $this->setAttribute('method', 'post');
    $this->setAttribute('data-parsley-validate', '');
    $this->setAttribute('data-elements', json_encode($this->form_elements));
    $this->setAttribute('autocomplete', 'off');
    for($i=0;$i<count($this->form_elements);$i++){
        $elements=$this->form_elements[$i];
        $this->add($elements);
    }
}
public function getInputFilter($action=false)
{
    if(!$this->inputFilter){
        $inputFilter = new InputFilter();
        $factory = new InputFactory();
        for($i=0;$i<count($this->form_elements);$i++){
            if(array_key_exists('validation',$this->form_elements[$i])){    
                $this->form_elements[$i]['validation']['name']=$this->form_elements[$i]['name'];
                $inputFilter->add($factory->createInput( $this->form_elements[$i]['validation'] ));
            }
        }
        $this->inputFilter = $inputFilter;
    }
    return $this->inputFilter;
}
}

how we remove case sensitivity of user name so both Harib or harib accepted?

Muhammad Arif
  • 1,014
  • 3
  • 22
  • 56
  • What database are you using? – drew010 Nov 17 '17 at 16:14
  • I used mongodb database – Muhammad Arif Nov 19 '17 at 15:44
  • In that case, you should create a [case-insensitive index](https://docs.mongodb.com/manual/core/index-case-insensitive/) for the username field so when the database adapter searches for it, it will find the record regardless of case. – drew010 Nov 19 '17 at 17:02
  • Hi @drew010 you see my controller code and form code as you seen i used zf2 authentication so please suggest according to this – Muhammad Arif Nov 19 '17 at 17:24
  • I believe the best solution is to fix your database index/collation so that lookups are case-insensitive. Otherwise you're going to need to fix all possible PHP code that looks up a username to filter it to lowercase and might run into other issues. If you fix it in one place at the DB level, none of your application code needs to change or have special cases to filter the usernames. If you want to go the code route, use Alain's code below; just make sure you don't allow a username to be inserted with any caps! – drew010 Nov 19 '17 at 19:18
  • @ArsalAli Case-insensitive queries in MongoDB are tricky. The simplest thing is a regex query with the /i flag. But IIRC, regex queries do not use an index, so they are problematic on large collections. Alternatives include using a fulltext index or `$strcasecmp`. Another solution would be to normalize into lowercase the field on which you wish to search - or duplicate that field into a normalized lowercase version - and search on that. Details provided in https://stackoverflow.com/a/7101938/131824 – David Weinraub Nov 22 '17 at 13:24

3 Answers3

2

Add a filter StringToLower in your loginform on the element user_id.

For this, the class that defines your loginform must implement InputFilterProviderInterface and you must add in the getInputFilterSpecification method as follows :

public function getInputFilterSpecification()
{
    return [
        'username' => [
            'name' => 'username',
            'required' => true,
            'filters' => [
                'name' => 'StringToLower',
                'name'=>'StripTags',
                'name'=>'StringTrim' 
            ],
            validators => [
                [
                    'name'=>'Regex',
                    'options'=> [
                        'pattern' => '/^[a-z0-9_.-]{1,50}+$/', 
                        'pattern_js' => '^[a-zA-Z0-9_\.\-]{1,50}$' 
                    ]
                ]
            ] 
        ],
        'password' => [
            'name' => 'password',
            'required' => true,
            'filters' => [
                array('name'=>'StripTags'),
                array('name'=>'StringTrim')
            ],
            'validators' => [
                [
                    'name'=>'Regex',
                    'options'=> [
                    'pattern' => '/^[a-z0-9_.-]{6,25}+$/', 
                    'pattern_js' => '^[a-zA-Z0-9_\.\-]{6,25}$' 
                    ]
                ]
            ]
        ]
    ];
}

So you are assured that the value returned in the post is in lowercase.

Alain Pomirol
  • 828
  • 7
  • 14
  • Hi @Alian Promirol I attached my login form code,can you tell me what i change in form? – Muhammad Arif Nov 16 '17 at 04:47
  • Personally, I prefer the `InputFilterProviderInterface` interface rather than `InputFilterAwareInterface`, I don't implement the `getInputFilter()` method unless I want to change the default behavior of ZF2 and I implement the `getInputFilterSpecification()` method as shown in my answer . – Alain Pomirol Nov 17 '17 at 16:02
2

Since you're using MongoDB, you could use a regex to get the user name from the database.

Suggestion 1:

In your example that would be:

db.stuff.find( { foo: /^bar$/i } );

Suggestion 2:

You can Use $options => i for case insensitive search. Giving some possible examples required for string match.

Exact case insensitive string

db.collection.find({name:{'$regex' : '^string$', '$options' : 'i'}})

Contains string

db.collection.find({name:{'$regex' : 'string', '$options' : 'i'}})

Start with string

db.collection.find({name:{'$regex' : '^string', '$options' : 'i'}})

End with string

db.collection.find({name:{'$regex' : 'string$', '$options' : 'i'}})

Doesn't Contains string

db.collection.find({name:{'$regex' : '^((?!string).)*$', '$options' : 'i'}})

More about regex in MongoDb here: https://docs.mongodb.com/manual/reference/operator/query/regex/index.html

Pascut
  • 3,291
  • 6
  • 36
  • 64
1

You may do this in two ways. Either you may create a custom authentication adapter or override a method of the default authentication adapter. I recommend that override that method which is easier than creating custom adapter.

So here is the method CredentialTreatmentAdapter::authenticateCreateSelect(). If you look up around 94 line (of zf 2.5) of that method from zend-authentication component then you would find the following line.

$dbSelect->from($this->tableName)
    ->columns(['*', $credentialExpression])
    // See the making of where clause
    ->where(new SqlOp($this->identityColumn, '=', $this->identity));

Here we are going to make our changes. Now lets override that method by extending Zend\Authentication\Adapter\DbTable. We would make a where clause which would search for both Harib or harib therefore. See the following extended CustomDbTable::class.

<?php
namespace Define\Your\Own\Namespace;

use Zend\Authentication\Adapter\DbTable;

class CustomDbTable extends DbTable
{
    protected function authenticateCreateSelect()
    {
        // build credential expression
        if (empty($this->credentialTreatment) || (strpos($this->credentialTreatment, '?') === false)) {
            $this->credentialTreatment = '?';
        }

        $credentialExpression = new SqlExpr(
            '(CASE WHEN ?' . ' = ' . $this->credentialTreatment . ' THEN 1 ELSE 0 END) AS ?',
            array($this->credentialColumn, $this->credential, 'zend_auth_credential_match'),
            array(SqlExpr::TYPE_IDENTIFIER, SqlExpr::TYPE_VALUE, SqlExpr::TYPE_IDENTIFIER)
        );

        // Here is the catch
        $where = new \Zend\Db\Sql\Where();
        $where->nest()
            ->equalTo($this->identityColumn, $this->identity)
            ->or
            ->equalTo($this->identityColumn, strtolower($this->identity))
            ->unnest();

        // get select
        $dbSelect = clone $this->getDbSelect();
        $dbSelect->from($this->tableName)
            ->columns(array('*', $credentialExpression))
            ->where($where); // Here we are making our own where clause

        return $dbSelect;
    }
}

Now custom authentication adapter is ready. You need to use this one inside the factory for authentication service instead of Zend\Authentication\Adapter\DbTable as follows

'factories' => array(

    // Auth service
    'AuthService' => function($sm) {
        $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');

        // Use CustomDbTable instead of DbTable here
        $customDbTable = new CustomDbTable($dbAdapter, 'tableName', 'usernameColumn', 'passwordColumn', 'MD5(?)');
        $authService = new AuthenticationService();
        $authService->setAdapter($customDbTable);

        return $authService;
    },
),

All are now set. That overridden method should be called whenever you call this one in your controller method:

$authResult = $authService->authenticate();

This is not tested. So you may need to change things where you need. Please fix those if needed.

Hope this would help you!

unclexo
  • 3,691
  • 2
  • 18
  • 26
  • Hi @unclexo i used mongodb not sql – Muhammad Arif Nov 22 '17 at 12:22
  • I did not notice that you're using mongodb. I am really sorry for that. However would you be able to show off `identityClass`? Or something where you're dealing with mongodb? – unclexo Nov 22 '17 at 14:27
  • Hi @unclexo I used zend authentication and you seen all my code above – Muhammad Arif Nov 24 '17 at 11:31
  • Well, the dealing with mongodb has been made within this service `doctrine.authenticationservice.odm_default` which you're using in your controller method. If you can find the condition for the username matching, you would then be able to apply your rules as @Pascut's answer. – unclexo Nov 24 '17 at 11:52