ajaxcodeigniter-4csrf-token

How to ensure continuous CSRF token validation for all AJAX requests in CodeIgniter 4?


Her I have created login form and with csrf token If i send first request it will validate properly. Now I tried with wrong passwod than new csrf token is received from server. And When Again i will resend request with correct password it will show error 403.

Here is my code. 1 . View File

<form action="<?=$module."/validate_login"?>" id="form" name="form">
                        <input type="hidden" class="txt_csrfname" name="<?= csrf_token() ?>" value="<?= csrf_hash() ?>" />
                        <div class="mb-4">
                            <label class="mb-1 text-dark">Email</label>
                            <input type="email" class="form-control form-control" id="username" name="username"
                                placeholder="hello@example.com">
                            <div class="invalid-feedback"></div>
                        </div>
                        <div class="mb-4 position-relative">
                            <label class="mb-1 text-dark">Password</label>
                            <input type="password" id="dz-password" name="password" class="form-control"
                                placeholder="Password">
                            <span class="show-pass eye">
                                <i class="fa fa-eye-slash"></i>
                                <i class="fa fa-eye"></i>
                            </span>
                            <div class="invalid-feedback"></div>
                        </div>
                        <div class="form-row d-flex justify-content-between mt-4 mb-2">
                            <div class="mb-4">
                                <div class="form-check custom-checkbox mb-3">
                                    <input type="checkbox" class="form-check-input" id="customCheckBox1">
                                    <label class="form-check-label" for="customCheckBox1">Remember my
                                        preference</label>
                                </div>
                            </div>
                            <div class="mb-4">
                                <a href="<?=base_url("forgot-password")?>" class="btn-link text-primary">Forgot
                                    Password?</a>
                            </div>
                        </div>
                       
                        <div class="text-center mb-4">
                            <button type="submit" class="btn btn-primary btn-block">Sign In</button>
                        </div>
                        <div class="form-row" id="alert-message" style="display: none;">
                            <div class="alert alert-danger alert-dismissible fade show">
                                <button type="button" class="btn-close" data-bs-dismiss="alert"
                                    aria-label="btn-close"><span><i class="fa-solid fa-xmark"></i></span>
                                </button>
                                <strong>Error!</strong> In-valid username or password.
                            </div>
                        </div>
                    </form>

2 . JS FILE

$("#form").validate({
    rules : {
        username : 'required',
        password : 'required'
    },
    messages : {
        username : 'Please enter e-mail',
        password : 'Please enter password'
    },
    submitHandler : function(form,e){
        e.preventDefault(); 
        var form_data = new FormData(form);
        $.ajax({
            url  : SITE_URL+"validate-login",
            type : "POST",
            contentType: false,
            cache: false,
            processData:false,
            data : form_data,
            beforeSend : function(){

            },
            success : function(response){
                $('.txt_csrfname').val(response.data.token);
                if(response.success == "1"){
                    $("#alert-message").css("display","none");  
                    window.location.href= SITE_URL+"Dashboard";                      
                }else{
                    console.log(response.data.token);
                    $("#alert-message").css("display","");
                }
            },
            error : function(){

            }
        });
    }
});
  1. Controller

    public function validate_login(){

     $request = service('request');
     $requestArr = $request->getPost();
     $responseArr = array("success"=>"1","data"=>array(),"message"=>"");
     $data = array();
     $data['token'] = csrf_hash();
     $ipaddress = $this->request->getIPAddress();
    
    
     $rules = [
         'username' => 'required|valid_email',
         'password' => 'required'
     ];
    
    if($this->validate($rules)){
         $userArr = $this->Auth->login($requestArr['username']);    
         if(!empty($userArr)) {
             $verify_password = password_verify($requestArr['password'],$userArr->password);
             if($verify_password){
                 $responseArr['success'] = "1"; 
                 $responseArr['message'] = "Loged in successfully.";
    
                 $this->session->set([                        
                     'tbl_users_id'=>$userArr->tbl_users_id,
                     'full_name'=>$userArr->full_name,
                     'username'=>$userArr->username,
                     'email'=>$userArr->email
                 ]);
    
                 $logArr = array(
                     'user_id' => $userArr->tbl_users_id,
                     'module' => $this->module,
                     'module_id' => $userArr->tbl_users_id,
                     'action' => "LOGIN",
                     'ip_address' => $ipaddress,
                     'remarks' => "User Loged In",
                     'raw_data'=> ""
                 );
    
                 insert_log($logArr);
             }else{
                 $responseArr['success'] = "0";
                 $responseArr['message'] = "In-valid username or password.";
             }
         }else{
             $responseArr['success'] = "0";
             $responseArr['message'] = "In-valid username or password.";
         }   
     }else{
         $responseArr['message']= "Required Fields are missing";
     }
    
     $responseArr['data']= $data;
     output($responseArr);
    

    }

It will validate CSRF Token only first call. After that it will show 403 error. Want to validate csrf token with all ajax request.


Solution

  • Well, it's most likely that you've set \app\Config\Security::$regenerate to true.

    Excerpt from \app\Config\Security

        /**
         * --------------------------------------------------------------------------
         * CSRF Regenerate
         * --------------------------------------------------------------------------
         *
         * Regenerate CSRF Token on every submission.
         */
        public bool $regenerate = true;
    

    This setting would regenerate the token for every ['POST', 'PUT', 'DELETE', 'PATCH'] HTTP request.

    In addition, in your Javascript source code, you tend to update the token: $('.txt_csrfname').val(response.data.token); in your AJAX success handler yet your Controller method validate_login() doesn't provide/send such information. Another issue I would raise is, what if it's not successful? Your AJAX error handler is empty, resulting in a token mismatch on the next POST HTTP request since you didn't update your front-end CSRF token on error.

    Solution 1:

    Disable \app\Config\Security::$regenerate.

     public bool $regenerate = false;
    

    Then, you won't have to update your CSRF token on every POST HTTP request ($('.txt_csrfname').val(response.data.token);).

    Solution 2:

    Enable \app\Config\Security::$regenerate and design an efficient mechanism to update your front-end CSRF token on every ['POST', 'PUT', 'DELETE', 'PATCH'] HTTP request. And the front-end CSRF token update must happen whether the request is successful or not.
    I provided such a solution in a question I answered previously. codeigniter4 code to add employee redirects to itself on submit when regenerate is set to true in config.security