Feb 2022: the wsurvey.adminLogon utility.

wsurvey.adminLogon is a php file and a javascript file -- that support slightly-secure admin logon
using a hashed (with crc32 or MD5) password.

wsurvey.adminLogon may be useful when admin access is needed in a php program that does a fair amount of
dynamic web retrieval (say, via AJAX).

Set up is straightforward. And usage is also straightforward, with simple defaults that can be modified by more
ambitious coders.

Contents:

I.    Setup and usage
I.a.     Setup details
I.b.     Usage
II.   Logon parameters ... callbackLogon, logonName, whenCall, and pwd
II.a.    How to specify logonName, callbackLogon, whenCall, and pwd
III.  Specifying logon attributes for different logonNames
IV.   Other functions, and global variables
IV.a.    Other useful javascript functions
IV.b.    Other useful php functions
IV.c.    Created variables
IV.d.    Global control variables
V.    Notes and disclaimer

          ------------------------

I.     Setup and usage

For simplicity, the following  assumes you install the wsurvey.adminLogon files into the directory containing
the php program that need slightly-secure admin logon. Note that you will need a version of jQuery.

The basic steps
  a) <script type="text/javascript" src="wsurvey.adminLogon.js"></script>. and a <script.. > to jQuery
  b) Create  wsurvey.adminLogon_params.php to specify a password, and other parameters
  c) Add a "logon" button to the HTML portion of your program -- a button that calls  wsurvey.adminLogon()
  d) Recommended: create a callback -- that is called on successful logon (or failure)
  e) If using in standalone programs: specify a path to wsurvey.adminLogon.php 

II.a. Setup details.

  a) in the main php program, include (in the <head> section)

       <script type="text/javascript" src="wsurvey.adminLogon.js"></script>

     and a version of jquery, such as

       <script type="text/javascript" src="jquery-3.6.0.min.js"></script>

     and, optionally, a link to an MD5 library. For example:

          <script type="text/javascript" src="md5b.js"></script>

        wsurvey.adminLogon will look for an md5 function, and call it as aMd5Hash=md5(aPasswordString).
            Thus, if your chosen library does not have a function named md5(), you should create a
            md5() function that calls whatever function appears in your md5 library.

        Note that if an md5 function is available, hashing of the password will use the md5 algorithim.
        If there is no md5 function available, hashing of the password will use the crc32 algorithim.


  b) Create a  wsurvey.adminLogon_params.php in the directory of the main php program.
     This specifies the password, and other variables.
     Example:

     <?php
       $passwordActual="myPass";       // the administrative password  (which will be entered by the client, and  hashed prior to sending to the server)
       $pwdDuration=2000;                     // in  seconds (pwd expires after this amount of time)
    ?>

     passwordActual is required.  Be sure to put its value in quotes!
     pwdDuration:   default (if not specified) is 2000

    While not strongly supported, you can specify multiple sets of these parameters for different "logonNames" --
    see below for the details.


   Notes:
     * If passwordActual='', an error occurs.
       This means: you MUST specify a non-empty passwordActual.
     * See the endNotes for a discussion of the security weaknesses of adminLogon -- including storage of passwords
       in the clear.

  c) Somewhere in your main .php program -- you will need to give the user an opportunity to logon!
     For example, in a button like

        <input  type="button" id="logonButton" value="Logon"   title="Admin logon"  onClick="wsurvey.adminLogon()" >

    Note   the "wsurvey.adminLogon() -- no arguments. This signals "first logon".

    Or you could be modern ...

        $('#logonButton').on('click',wsurvey.adminLogon) ;
                    ...
        <input  type="button"  style="opacity:0.0" id="logonButton" value="Logon"  pwd="">

      Note  the ''pwd="" : this signals "first logon".

     Either of these yield  a simple default -- a small box (containing a password input form) is placed in the document.
     The "logonName" is "default".

     If an incorrect password is entered, this small box is refreshed. And the user tries again.
     Once a correct password is entered, the logon completes and this small box is removed.
     Or the user can give up and close this small box.

     You can make a number of modifications by specifying several parameters:
       *  callbackLogon : the name of a function to callback on success, or failure.
       *  whenCall: when to call the callback.
       *  logonName: the "logonName" -- which could be a username, or more typically the application this logon refers to.
       *  pwd : a default password -- typically, a '' to "start the logon process"

     See "Logon parameters ... " for the details on how to specify these parameters.

 d) Optional, but recommended. Create a callback javascript function -- say, my_logonCallback
     callbackLogon should be the name of this function
     It is called after wsurvey.adminLogon gets a response from the server -- though not always (see whenCall).

     If enabled it is called using:
        callbackLogon(amode,logonName,timeLeft)
     where :
        amode: one of  'no', 'yes', 'prior', 'expired', or 'new'  -- see below for the details.
        logonName: the logonName parameter -- see below for the details
        timeLeft: amount of time left until password expiry (in seconds). If expired, or not currently loged on, this will be 0

     Example:

       function my_logonCallback(amode,logonName,timeLeft) {
          $(document).data('logonExpiry',timeLeft);           // somewhat redundant -- see description of created variables!
          if (timeLeft>0) $('#logonStatus').html('Logon successful for '+logonName);
          return 1;
       }

       See below for the details .

  e)  If using in a standalone programs: specify a path to wsurvey.adminLogon.php
  
      The javascript function wsurvey.adminLogon() sends an "ajax" request to the server using
      wsurvey.adminLogon.php.
      By default, wsurvey.admnLogon() uses a url of: /wsurvey/wsurvey.lib/php/wsurvey.adminLogon.php
         This default assume that the main php program (that uses wsurvey.adminLogon) is in a www tree
         that has /wsurvey as a subdirectory.
      If this is NOT the case -- say, you are using wsurvey.adminLogon.js and wsurvey.adminLogon.php in
      a standalone program -- you MUST specify where to find wsurvey.adminLogon.php

      This is done with a `wsurveyadmin_php` global javascript variable.
      `wsurveyadmin_php` should be a string variable, containing either an absolute selector, or a 
      relative selector

      Example, assuming a request to /mySite/funProgs/prog1.html, and the main www site is D:\www:
         wsurveyadmin_php='srcPhp/wsurvey.adminLogon.php' ;
      or
        wsurveyadmin_php='/mySite/funProgs/srcPhp/wsurvey.adminLogon.php' ;
      are equivalent and point to a D:\www\mySite\funProgs\srcPhp\wsurvey.adminLogon.php

      Note: `wsurveyadmin_php` must be a global variable. To assure that it is global, you can 
             specify window['wsurveyadmin_php']='selector_to_wsurveyAdminLogonPhp' ;

I.b. Usage:

  Under the simple, usage is automatic. The client clicks on the logon button, a submit password container is displayed.
  and she then provides a password.  On success, the password entry container is hidden.

  Regardless of whether a callbackLogon is specified:
     On success, a php $_SESSION variable, and a  $(document).data() variable, are created.
     These contain information on logon status.  As described below, you can access them directly, or use
     wsurvey.adminLogon_status(...).

  This may not be a very useful end point -- in many circumstances the  callbackLogon is useful!

          ------------------------

II. Logon parameters ... callbackLogon, logonName, whenCall, and pwd

  By default -- if no parameters are specified -- the logon menu that is automatically created & displayed
  (in a small container) is very simple. And what happens after success or failure is simple.

    * A small container is written in the middle of the browser window.
      It contains a password entry field, and a submit button

    * Clicking the submit button sends the (hashed) password to the server.

    * If the password is correct, a $_SESSION and a $(document).data() variable are saved

    * If it is incorrect, the small container is redisplayed, with a "Incorrect password" header

    * This will repeat, there is no limit on attempts. Though the client can close the container.

  You can modify this by specifying the logon parameters:  callbackLogon, logonName, and whenCall

  See the next section for the several ways you can specify these variables!

   *  callbackLogon : a string name of a function. As described above:
        After each call to wsurvey.adminLogon(), it may be called using:
            callbackLogon(amode,logonName,timeLeft)
        where :
           amode:  'no', 'yes', 'prior', 'expired', or 'new'
           logonName: the logonName provided (see below).
           timeLeft: amount of time left until password expiry (in seconds).
                    If expired, or not currently loged on, this will be 0

      Technical note: window[callbackLogon](amode,logonName,timeLeft) is used.
                     That means callbackLogon has to the name of a function. It can NOT be a method in an object.
                     Thus, something like 'wsurvey.adminLogon.defaultCallback' would not work.

   * whenCall : When to call the callbackLogon
       0 or 1 or 2. If 0 (the default), only call the callbackLogon on success.

          whenCall= 0, : a failure results in a redisplay of the password entry container
          amode can be yes, or prior -- since failures do NOT lead to the use of callbackLogon!

          whenCall= 1:  every call to wsurvey.adminLogon() ends with a callbackLogon
            That means the callbackLogon must decide what to do, typically using the value of amode.
            For example, the values of amode mean:
              * 'prior' : success -- a prior logon is still active. This shortcuts the need to create
                     a password entry field
              * 'yes' : success -- a new logon, after entry of a correct password
              * 'no' : failure. Ask client to reenter?
              * 'expired': was logged on, but expired -- no password so no attempt to check
              * 'new'  :  No prior logon active,  no password so no attempt to check

          whenCall= 2 : call after success or failure, but not on first call.
              On first call, amode can be:   'prior', 'expired', or 'new'
              On later calls, amode can be: 'yes' or 'no'
              This the same as whenCall=0, except the callbackLogon is called even on failures.
                 That means the auto-generated password entry container is created, and removed.

              The callbackLogon could do nothing on failures, and let the client try again.
              Or, it could take action (such as counting failures).

           In contrast: for whenCall=1, you are responsible for creating and removing password entry containers.

   *  logonName : a string specifying what "logonName" to use when looking for logon parameters.
      If not specified, 'default' is used.
       If specified, and it does not exist:
          * the wsurvey.adminLogon_params.php file does NOT specify "logonName" specific variables, 'default' is used.
          * otherwise, an error occurs
          
    * pwd: the password to use. 
       This has one intended use: pwd='' signals "first time call". Which means the password entry container will
      not have a "please try again" message.
      In particular: if not specified, the .val() of the element that invoked wsurvey.adminLogon will be used, which could a descriptive
      title! Thus: if pwd is specified, its value is used INSTEAT of a $(whatever).val()

      Note: while one could specify an actual password, that would be a security risk -- its too easy for someone to read 
             the file and learn the password!

  Example of a callback (say, used with whenCall=2):

       function my_logonCallback(amode,logonName,timeLeft) {
          if (timeLeft==0)  { // failure
              $('#logonStatus').html('Please reenter your password');
          } else {
             $(document).data('logonExpiry',timeLeft);           // somewhat redundant -- see description of created variables!
             let e1= $('#logonStatus').html('Logon successful for '+logonName);
          return 1;
       }


     Technical note: when wsurvey.adminLogon automatically creates a  password entry container
        * The container has a name of "wsurveyadminLogon_passwParent"
        * The password field has a name of "wsurveyadminLogon_passw"
        * The submit button has a name of "wsurveyadminLogon_submit"

	   ------------------------

II.a. How to specify logonName, callbackLogon, whenCall, and pwd

 There are several means of doing this.


  a) In a direct call to wsurvey.adminLogon(apwd,callbackLogon,logonName,whenCall)

   All 4 arguments should be specified, but you can use '' or 0 as defaults:
    * callbackLogon ='' or 0 : no callback
    * logonName= '' or 0  : 'default'
    * whencall not 0, 1, or 2 : 0

    For apwd, ''  (or 0) means "check if prior logon."
    Then: if no prior, and if whenCall=0 or 2, display the auto-generated password entry container.

    Example:
        <input  type="button" id="logonButton" value="1st Logon"  onClick="doLogonCall()" >

        ....
        function doLogonCall(athis) {
            wsurvey.adminLogon('','myLogonCallBack','prog3',0 )  ; // call myLogonCallBack only on success.
        }


  b) Using logonName, callbackLogon, and whenCall attributes in the <input> field.

     If whenCall=0 or 2, a password entry container is written to the browser.

     Example:
        <input  type="button" id="logonButton" value="Logon"  title="My logon"
            logonName="myApp2" whenCall="2" callbackLogon="myLogonCallBack"  pwd="" onClick="wsurvey.adminLogon(this)" >

      You can specify zero, one, two ,or three of these attributes (the default values are used if not specified)

      Note the use of the pwd attribute. If specified, its value is used rather than the value of the input.
          It is NOT a good idea to have an actual password in this attribute --
          but using 'pwd=""' will force  a "first logon"  (useful if whenCall=0 or 2)

  c) Using a jQuery .on call.

     Similar to (b), but not using attributes.

     Example:
        <input  type="button" id="logonButton" value="Logon"  title="My logon"  >
       ...

       let data={'whenCall':1,'callbackLogon':'myLogonCallBack','logonName':'app3','pwd':''};
       $('#myLogonBox').on('click',data,wsurvey.adminLogon);
           ...

  Note that you can use .on() with attributes specified in the <input> field.

  And a value in the 'data' argument overrides a value specified in an attribute.
            ...

  Notes:
     * In the 4 argument versions, with apwd specified (not equal to '').
       The apwd is hashed and sent to the server.

    *  If callbackLogon is not specified, success (or failure) is silent

    *  If apwd='', and whencall= 0 or 2, this is a "please create password entry container" request!

    *  If apwd='' and whenCall=1. Check for a current logon. If not currently logged on,
         a callbackLogon('0',logonName,0) occurs -- nothing else is done.
         IT IS UP TO THE callbackLogon to display a password entry form!

   ------

III. Specifying logon attributes for different logonNames

  In wsurvey.adminLogon_params you should specify the default passwordActual, and pwdDuration.
  These are used for the 'default' logonName. Note that if you do not specify pwdDuration, a value of 2000 is used.

  You can also specify one or more logonNames specific attributes using this syntax:

    $wsurveyadminLogon_params[aLogonName']=['passwordActual'=>'something',$pwdDuration'=>aNumber];

   For example, the following creates two entries (in addition to the default).

        $passwordActual='xxx' ;
        $pwdDuration=2500  ;

        $wsurveyadminLogon_params['dogApp']=['passwordActual'=>'fido35','pwdDuration'=>1500];
        $wsurveyadminLogon_params['imageEdit']=['passwordActual'=>'deluxe515'];

     Note that in imageEdit, pwdDurations is not specfied; so the default (2500) is used.

     Caution: if you specify any $wsurveyadminLogon_params[] entries, and you logon with a logonName that does
              NOT match one of these entries (and is not 'default') -- an error will occur.

              In contrast: if you specify just the defaults, but use wsurvey.adminLogon with a logonName that isn't
              'default' --  the defaults are used for all of them (an error will not occur)


               ------------------------

IV. Other functions, and global variables
  There are several javascript and php functions that are useful. And some global status and control variables!

IV.a.  Other useful javascript functions

 wsurvey.adminLogon_logoff(logonName,'callbackLogoff');
       logoff (on client and server side). Calls 'callbackLogoff' (string name of function) on completion as:

  Example:
    wsurvey.adminLogon_logoff(logonName,'logoffDone');

      If logonName ='', 'default' is used.
      If callbackLogoff='', no callback is attempted. A success, or failure, message will be written to console.log().
            ...

    function logoffDone(logonName,acontent) {
       $('#logonButton').show();
       $('#logoffButton').hide();
       $(document).data('logonStatus',0);
       return 1;
    }

    Note: this is NOT the same as callbackLogon!

   ------

 wsurvey.adminLogon_status(logonName,'callbackStatus')

   Returns information on logon (for the logonName specified).
   The source of the information depends on the value of  acallback.
      callbackStatus : 0 or not specified= read from local (javascript) variables (that were saved during a recent logon)
      callbackStatus : string name of a call back function -- read from php variables, and return information using acallback

   The "function" method is more accurate. In particular, before first logon the "local" method will have no information
   available, hence will assume "not logged on" (even though the client may still have an active session).

   In either case the information returned in a 2 element array: [timeLeft,status]
           timeLeft: 0 (if 'no' logon, or 'expired', logon). Or seconds remaining if 'yes'
                     This is relative seconds (from time of submission). Not an absolute expiry time.

           status: either 'no','yes', or 'expired'

    If callbackStatus=0, this is returned directly to the caller as:
        [timeLeft,status]
    Otherwise: the callbackStatus is called using
        window[callbackStatus]([timeLeft,status])

  Example 1:
      logonInfo=wsurvey.adminLogon_status('myProg');
      alert('Local logon info: '+logonInfo.join(', '));

  Example 2:
    input  type="button" id="logonStatus" value="LogonStatus = n.a."   title="Check logon (server status)"
           onClick="checkMyLogon()">
           .. ... ...
    function checkMyLogon(foo) {      // get from server
        wsurvey.adminLogon_status('myProg','checkMyLogon2');   // query server for actual status
    }
    function checkMyLogon2(hh) {  // h[0]=timeleft (0 if not logged on), hh[1]=no,yes,expired
       var sayit ;
       if (hh[0]>0) {
          sayit='expires in '+(hh[0] ).toFixed(0);
      } else {
           sayit=hh[1];
      }
      $('#logonStatus').html('Status '+sayit) ;
    }

    Note: callbackStatus is NOT the same as callbackLogon

   ------

  wsurvey.adminLogon_extend(seconds,logonName,message)
     Extend the logon expiry, for logonName, by this many seconds. 
     If logonName not specified, 'default' is used.
     Message is optional. If specified, and not '', it is also written to console.log().

     If not logged on, nothing happens.
     However, if recently logged on (and not explicitly logged off), this will auto-logon (no need to re-enter password).

     Note that a short report, that can include the message, is written to console.log(). 
     Nothing  else is returned -- there is no callback for extend.

     If a silent failure (expiry not extend because not logged on) is a problem: use wsurvey.adminLogon_status()

     Hint: after an extend, use wsurvey.adminLogon_status to view timeLeft!

          ------------------------

IV.b.  Other useful php functions


  $astat=wsurvey\adminLogon\logonStatus($logonName)

    Returns a 2 element array [logonStatus,secondsLeft]
    where logonStatus can be:
      0 : not logged on
      1 : currently logged on
      2 : was logged on, but is now expired
     secondsLeft is non-zero if logonStatus=1. It is the seconds until this logon expires

     Note: this can be useful in AJAX calls, to check whether or not someone is still (or ever) logged on.
     However, it does involve an include, or require, of wsurvey.adminLogon.php

     A shortcut is to use something like:

        $nowTime=time();
        $sessVar='wsurveyadminLogon_status_'.$logonName ;
        $logonActive= (array_key_exists($sessVar,$_SESSION) && $_SESSION[$sessVar]>$nowTime)   ? 1 : 0 ;

 Note:
    At the top of the .php script, you can specify a 'use' to access the wsurvey\adminLogon 'php namespace'
    (that is hardcoded in wsurvey.adminLogon.php)
    For example:
      use wsurvey\adminLogon as doLogon;

          ------------------------

IV.c. Created variables

On a succesful logon, these "logonName" specific variables are saved.

A $(document).data(statusName) variable is created, where statusName='wsurveyadminLogon_status_'+logonName
  It is [] if not currently logged on.
  If logged on, it is [timeLeft,expiryTime,expiryTimeStamp]
     timeLeft : seconds until expiry
     expiryTime : unix epoch second when expiry -- javascript version (milliseconds).
                 This is an ABSOLUTE time, not a  seconds-until-expiry
     expiryTimeStamp: time stamp (m/d/y hr:min:sec) of expiry

  Notes:
    * this set is when the server confirms a successful logon.
       Hence, timeLeft is as of the moment of logon (not as of the current moment).
     * logonName='default' if logonName was not specified
     *   You can use wsurvey.adminLogon_status(..) to read its expiry value.

     Example:  let localLogonStatus=$(document).data('wsurveyadminLogon_status_default');

A $_SESSION variable, 'wsurveyadminLogon_status_'.$logname (logonName='default' if not specified) is created.

   It is empty at first, but upon logon contains the expiry time (in seconds): timeOfLogon + pwdDuration.
   This is an ABSOLUTE time, not a seconds-until-expiry
   Or, it will be 0 if NOT currently logged on.

   Example: $expireIn=$_SESSION['wsurveyadminLogon_status_default'];

 Note:
   *  You can use wsurvey.adminLogon_status(..) to read either the "local" or "sever"  expiry value.


IV.d. Global control variables

    These are are not necessary, but can be use to tweak some of the output.

    If specified, they should be specified in the main program (since they will be read as globals).

     var wsurveyadminlogon_quiet : if specified, and 1, do NOT write information to console.log()

        In particular, this suppression will occur when
           * wsurvey.adminLogon, with callbackLogon='' :
           * wsurvey.adminLogon_extend
           
     var wsurveyadmin_logonClass : if specified, should be the name of an existing css class
         This will be used to format the outer container that contains the auto-generated password entry & submit fields.
         If not specified, a generic fixed-position styling is used.

        ------------------------

V.  Notes and disclaimer

  * It is now advised that MD5, and certainly CRC32, are not strong techniques for encryptying passwords.
    Both (especially CRC32) are vulnerable to brute force attacks, given the speed of modern processors and GPUs.

  * The use of wsurvey.adminLogon_params.php to store unencrypted passwords is a vulnerablity.
      Someone with directory access will be able to read the file, hence the password.
      One small protection: as a .php file,  web access (via a URL) will return an empty file.

 *  In other words: non-state-of-the-art hashing, unencrypted password storage, and other flaws are reasons why
    wsurvey.adminLogon is "slightly-secure".

  * IOW: wsurvey.adminLogon will hinder man in the middle attacks, but is not secure against serious attack.
        So don't use it where security is important (i.e.; for financial transactions).

  * With some clever coding you could use wsurvey.adminLogon to provide access to a set of users.
      Say: by using your own logonForm, that first asks the user to enter a username and then directly calls
          wsurvey.adminLogon(apwd,'myLogonCallback',enteredUser,1);

      However, that would require lengthy modifications to the wsurvey.adminLogon_params.php file -- which could be clumsy and
      slow if there are a lot of users.

      Thus: wsurvey.adminLogonis intended for a limited set of "admin" logons -- for a limited set of applications that
            would benefit from allowing the administrator to make changes without setting up an elaborate user access
            control system.

Contact: Daniel Hellerstein. danielh@crosslink.net. 
       http://www.wsurvey.org/distrib, or  https://github.com/dHellerstein 

Disclaimer:

    wsurvey.adminLogon is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    wsurvey.adminLogon is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    If you did not receive a copy of the GNU General Public License,
    see <http://www.gnu.org/licenses/>.
