Tuesday, May 3, 2011

ExtJS / PHP - Login form and user authentication

Something that's needed time and time again is a login page that provides user authentication. I got the original PHP code for this from marakana.com. I created a front end using ExtJS and modified the code to store an md5 hash of the password in a flat file. As some point I'd like to add an install script to create a SQL database and store the password there instead.

This is meant for smaller sites / applications where SSL is deemed to complex or expensive.

Security is provided using a 'Challenge-Response Authentication Method'. Sending a password using clear text provides an easy opportunity for it to be sniffed by an attacker. The next step is to encrypt the password but this would could still allow access through a reply attack. In the code below, the server generates a random string which it then sends to the client. The client then combines this with the password and returns it to the server for authentication.

I've included sample code in yourpage.php which allows the user to change their user name and password. The changes are carried out on ther server-side by settings.php.

As well as the files below, you will also need Paj's MD5 algorithm available here;

http://pajhome.org.uk/crypt/md5/


authenticate.php
<?php 
  function getPasswordForUser($username) {
    $config_file = file_get_contents("config.json");
    $config_array = json_decode($config_file, true);
    return $config_array["config"][0]["password"];
  }
  function validate($challenge, $response, $password) {
    return md5($challenge . $password) == $response;
  }
  function authenticate() {
    if (isset($_SESSION[challenge]) && isset($_REQUEST[username]) && isset($_REQUEST[response])) {
      $password = getPasswordForUser($_REQUEST[username]);
      if (validate($_SESSION[challenge], $_REQUEST[response], $password)) {
        $_SESSION[authenticated] = "yes";
        $_SESSION[username] = $_REQUEST[username];;
        unset($_SESSION[challenge]);
      } else {
        echo '{"success":false,"message":"Incorrect user name or password"}';
        exit;
      }
    } else {
      echo '{"success":false,"message":"Session expired"}';
      exit;
    }
  }
  session_start();
  authenticate();
  echo '{"success":true}';
  exit();
?>


common.php
<?php
  session_start();
  function is_authenticated() {
    return isset($_SESSION[authenticated]) && $_SESSION[authenticated] == "yes";
  }
  function require_authentication() {
    if (!is_authenticated()) {
      header("Location:index.php");
      exit;
    }
  }
?>


index.php
<?php
  session_start();
  session_unset();
  srand();
  $challenge = "";
  for ($i = 0; $i < 80; $i++) {
    $challenge .= dechex(rand(0, 15));
  }
  $_SESSION[challenge] = $challenge;
?>
<html>
  <head>
     <title>Login</title>
     <link rel="stylesheet" type="text/css" href="
http://extjs.cachefly.net/ext-3.3.0/resources/css/ext-all.css" />
     <!-- Calls to ExtJS library files from Cachefly. -->
     <script type="text/javascript" src="
http://extjs.cachefly.net/ext-3.3.0/adapter/ext/ext-base.js"></script>
     <script type="text/javascript" src="
http://extjs.cachefly.net/ext-3.3.0/ext-all-debug.js"></script>
     <script type="text/javascript" src="md5.js"></script>
     <script type="text/javascript">
       Ext.BLANK_IMAGE_URL = 'images/s.gif';
       Ext.onReady(function(){
         var loginForm = new Ext.form.FormPanel({
           frame: true,
           border: false,
           labelWidth: 75,
           items: [{
             xtype: 'textfield',
             width: 190,
             id: 'username',
             fieldLabel: 'User name'
           },{
             xtype: 'textfield',
             width: 190,
             id: 'password',
             fieldLabel: 'Password',
             inputType: 'password',
             submitValue: false
           },{
             xtype: 'hidden',
             id: 'challenge',
             value: "<?php echo $challenge; ?>",
             submitValue: false
           }],
           buttons: [{
             text: 'Login',
             handler: function(){
               if(Ext.getCmp('username').getValue() !== '' && Ext.getCmp('password').getValue() !== ''){
                 loginForm.getForm().submit({
                   url: 'authenticate.php',
                   method: 'POST',
                   params: {
                     response: hex_md5(Ext.getCmp('challenge').getValue()+hex_md5(Ext.getCmp('password').getValue()))
                   },
                   success: function(){
                     window.location = 'yourpage.php';
                   },
                   failure: function(form, action){
                     Ext.MessageBox.show({
                       title: 'Error',
                       msg: action.result.message,
                       buttons: Ext.Msg.OK,
                       icon: Ext.MessageBox.ERROR
                     });
                   }
                 });
               }else{
                 Ext.MessageBox.show({
                   title: 'Error',
                   msg: 'Please enter user name and password',
                   buttons: Ext.Msg.OK,
                   icon: Ext.MessageBox.ERROR
                 });
               }
             }
           }]
         });
         var loginWindow = new Ext.Window({
           title: 'Login',
           layout: 'fit',
           closable: false,
           resizable: false,
           draggable: false,
           border: false,
           height: 125,
           width: 300,
           items: [loginForm]
         });
         loginWindow.show();
       });
     </script>
   </head>
   <body>
   </body>
</html>


config.json
{
  "config":[{
    "username":"admin",
    "password":"21232f297a57a5a743894a0e4a801fc3"
  }]
}


yourpage.php
<?php
  require("common.php");
  require_authentication();
?>

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Your Page</title>

    <link rel="shortcut icon" href="favicon.ico" />
    <link rel="stylesheet" type="text/css" href="http://extjs.cachefly.net/ext-3.3.0/resources/css/ext-all.css" />
    <link rel="stylesheet" type="text/css" href="editor.css" />

    <!-- Calls to ExtJS library files from Cachefly. -->
    <script type="text/javascript" src="http://extjs.cachefly.net/ext-3.3.0/adapter/ext/ext-base.js"></script>
    <script type="text/javascript" src="http://extjs.cachefly.net/ext-3.3.0/ext-all.js"></script>
    <script type="text/javascript" src="md5.js"></script>
    <script type="text/javascript" src="editor.js"></script>

    <script type="text/javascript">
      function changeSettings(){
        var settingsDialog = new Ext.Window({
          title: 'Settings',
          id: 'settingsdialog',
          border: false,
          height: 140,
          layout: 'fit',
          resizable: false,
          width: 300,
          items: [{
            xtype: 'form',
            frame: true,
            border: false,
            labelWidth: 75,
            items: [{
              xtype: 'textfield',
              id: 'input_username',
              fieldLabel: 'User name',
              allowBlank: false,
              width: 190
            },{
              xtype: 'textfield',
              id: 'input_password',
              fieldLabel: 'Password',
              inputType: 'password',
              allowBlank: false,
              width: 190
            }]
          }],
            buttons: [{
              text: 'Submit',
              id: 'save_config',
              handler: function(){
                if(Ext.getCmp('input_username').isValid() && Ext.getCmp('input_password').isValid()){
                  Ext.Ajax.request({
                    url: 'settings.php',
                    params: {
                      username: Ext.getCmp('input_username').getValue(),
                      password: hex_md5(Ext.getCmp('input_password').getValue())
                    },
                    success: function(response, opts) {
                      var obj = Ext.decode(response.responseText);
                      if (obj.success) {
                        Ext.MessageBox.show({
                          title: 'Your Page',
                          msg: 'Your changes have been saved',
                          buttons: Ext.MessageBox.OK,
                          icon: Ext.MessageBox.INFO
                        });
                      }else{
                        Ext.MessageBox.show({
                          title: 'Your Page',
                          msg: 'Unable to save changes',
                          buttons: Ext.MessageBox.OK,
                          icon: Ext.MessageBox.ERROR
                        });
                      }
                    },
                    failure: function(response, opts) {

                    }
                  });
                } else {
                  Ext.MessageBox.show({
                    title: 'Your Page',
                    msg: 'Please enter user name and password',
                    buttons: Ext.MessageBox.OK,
                    icon: Ext.MessageBox.ERROR
                  });
                }
              }
            },{
              text: 'Close',
              handler: function(){
                settingsDialog.close();
              }
            }]
        });

        settingsDialog.show();
      }
      Ext.onReady(function(){
        Ext.QuickTips.init();
        var contentPanel = new Ext.Panel({
          frame: true,
          layout: 'fit',
          items: [{
            xtype: 'textarea'
          }],
          tbar: [{
            xtype: 'button',
            text: 'Settings',
            tooltip: 'Change Settings',
            handler: function(){
              changeSettings();
            }
          },{
            xtype: 'button',
            text: 'Logout',
            tooltip: 'Logout',
            handler: function(){
              window.location = 'index.php';
            }
          }]
        });

        var viewport = new Ext.Viewport({
          layout: 'fit',
          items: [contentPanel]
        });

      });
    </script>
  </head>
  <body>
  </body>
</html>


settings.php
<?php
  $config_json = "{
  \"config\":[{
    \"username\":\"" .$_POST['username']. "\",
    \"password\":\"" .$_POST['password']. "\"
  }]
}";

  file_put_contents('config.json', $config_json);
  echo "{\"success\":true}";

?>


Note: User name and password both set to 'admin' in the above example.

At some point in the future I aim to extend this to allow multiple users.

A couple of screen shots;

Login window


User not authenticated


Wednesday, March 30, 2011

WebEditor - Web server based text editor

I do most of my coding during the many quiet spells at work. As you'd expect we can't install software on our work PCs so I don't have the luxury of being able to use an IDE.

In the past I have used a browser based ftp client such as net2ftp.com to download the file I'm working on, modify it in Notepad and then upload it for testing.

I then came across WebPad (http://dentedreality.com.au/projects/webpad/) which allowed me to modify the files making up my web sites / applications directly on the server. However, it does not like having multiple instances open at the same time which caused me a few problems when I first found out. When working on a web app it is helpful to be able to have the HTML / Javascript / PHP / CSS files all close to hand.

As such, I started work on an ExtJS based, tabbed, web server housed, text editor.


I will aim to put updates here as I reach definite milestones.

Tuesday, March 29, 2011

ExtJS - Confirm close of tab in tabpanel

Couldn't find a complete solution when trying to solve this problem so here's how I did it.

A handler is added for the 'beforeclose' event. Due to the Ext MessageBox being asynchronous, the confirm box will show while the function returns false and cancels the 'close' event of the tab. If 'no' is selected (the user does not wish to save changes) then the tab is removed from it's parent container. Can be used to extend any type of component you are using in your tabpanel.

Hope this saves someone some time and trouble.


Ext.ConfirmPanel = Ext.extend(Ext.Panel, {

  initComponent: function(){

    Ext.ConfirmPanel.superclass.initComponent.apply(this, arguments);

    this.addListener({
      beforeclose:{
        fn: this.onClose,
        scope: this
      }
    });


  },

  onClose: function(p){
    Ext.MessageBox.show({
      title: 'Save changes?',
      msg: 'Do you want to save changes?',
      buttons: Ext.MessageBox.YESNOCANCEL,
      fn: function(buttonId){
        switch(buttonId){
          case 'no':
            this.ownerCt.remove(p);   // manually removes tab from tab panel
            break;
          case 'yes':
            this.saveToFile();
            this.ownerCt.remove(p);
            break;
          case 'cancel':
            // leave blank if no action required on cancel
            break;
        }
      },
      scope: this
    });
    return false;  // returning false to beforeclose cancels the close event
  }

  saveToFile: function(){
    //your code to save changes here
  }

});

Ext.reg('confirmpanel', Ext.ConfirmPanel);  // register the extension as an xtype

var yourTabPanel = new Ext.TabPanel({
  activeTab: 0,
  items: {
    xtype: 'confirmpanel',   // using the new xtype as your tab panel item will give the confirm function on closing
    closable: true
  }
});
 
Please leave a comment if this code helped you or if you have anything to add.