Menu

HOWTO: Digest Access Authentication w Apache

Daniel
2011-04-21
2013-05-20
  • Daniel

    Daniel - 2011-04-21

    The default authentication scheme in limbas is HTTP Basis Access Authentication. Therefore, if you are not using encryption through SSL or SSH the credentials are sent as plain text (okay, it is Bas64 encoded, but NOT ENCRYPTED) on each and every request. http://tools.ietf.org/html/rfc2617

    One may handle HTTP Basic Access Authentication with PHP (since 5.1.0 also Digest). But you have to protect your script against SQL-Injection and so on. http://php.net/manual/en/features.http-auth.php

    There is no universal way to log out of a HTTP-Auth-Session. The solution is very Browser dependent. http://www.berenddeboer.net/rest/authentication.html

    The Apache HTTP-Server can check the Digest credentials against a SQL-Database using the modules dbd, auth_digest and authn_dbd. http://httpd.apache.org/docs/2.2/mod/mod_authn_dbd.html

    After applying the following modifications ALL passwords stored in lmb_userdb are invalid. Be sure to have access to that table to be able to reset the password for the Administrator. The authentication realm is used three times and must be exactly the same: both AuthName directives and in the SQL-UPDATE statement between the colons.

    Apache Configuration
    The virtual host also provides a SVN-Repository with single-sign-on.

    NameVirtualHost *:80
    <VirtualHost *:80>
       DocumentRoot /opt/openlimbas/dependent
       
       DBDriver        pgsql
       DBDParams       "dbname=limbas user=wwwrun password=wwwrunpasswd"
       DBDMin          4
       DBDKeep         8
       DBDMax          10
       DBDExptime      300
       
       <Directory /opt/openlimbas/dependent>
          Options FollowSymlinks
          RewriteEngine On
          RewriteCond %{QUERY_STRING} (.*&)?(logout=1)(&.*)?
          RewriteRule (.*) /logout.php? [R,L]
          ErrorDocument 401 /logout.php
          
          Order Allow,Deny
          Allow from All
          
          AuthType Digest
          AuthName "ACME Ltd. - bitJournal"
          AuthDigestProvider dbd
          AuthDBDUserRealmQuery "SELECT passwort, EXTRACT(EPOCH FROM NOW()) AS TIME FROM lmb_userdb WHERE username = %s AND (VALIDDATE >= CURRENT_TIMESTAMP OR VALID = FALSE) AND lock = FALSE AND del = FALSE;"
          Require valid-user
       </Directory>
       
       <Location /svn>
          DAV svn
          SVNReposName bitJournal
          SVNPath /srv/svn/repos/openlimbas
          AuthzSVNAccessFile /srv/svn/authz
          
          AuthType Digest
          AuthName "ACME Ltd. - bitJournal"
          AuthDigestProvider dbd
          AuthDBDUserRealmQuery "SELECT passwort FROM lmb_userdb WHERE username = %s AND (VALIDDATE >= CURRENT_TIMESTAMP OR VALID = FALSE) AND lock = FALSE AND del = FALSE;"
          Require valid-user
       </Location>
    </VirtualHost>
    

    Database modifications

    CREATE ROLE www NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE;
    GRANT SELECT ON TABLE lmb_userdb TO www;
    CREATE ROLE wwwrun LOGIN PASSWORD 'wwwrunpasswd' NOSUPERUSER NOINHERIT NOCREATEDB NOCREATEROLE;
    GRANT www TO wwwrun;
    UPDATE lmb_userdb SET passwort = md5('admin:ACME Ltd. - bitJournal:limbas') WHERE user_id = 1; /* {username}:{realm}:{password} */
    

    file system modifications

    ln -s ../limbas_src/logout.php /opt/openlimbas/dependent/logout.php
    

    Source code modifications

    Index: lib/session.lib
    ===================================================================
    --- lib/session.lib (revision 4)
    +++ lib/session.lib (working copy)
    @@ -15,9 +15,28 @@
     /* --- Beschr.: Session-Libary --------------------------------------- */
     /* --- ID: 38 -------------------------------------------------------- */
    
    +if ($_SERVER['AUTH_TYPE'] == 'Digest' && $_SERVER['AUTHENTICATE_TIME']) {
    +   if ((abs($_SERVER['AUTHENTICATE_TIME'] - $_SERVER["REQUEST_TIME"]) < 2) &&
    +       (preg_match('/username="(.*?)", *realm="(.*?)"/', $_SERVER["PHP_AUTH_DIGEST"], $auth_realm) == 1)) {
    +       $auth_trust = true;
    +       $auth_user = parse_db_string($auth_realm[1],30);
    +       $auth_realm = $auth_realm[2];
    +       $auth_pass = '';
    +   } else {
    +       //TODO: handle attack
    +       header("HTTP/1.1 403 Access Denied");
    +       die("403 Access Denied");
    +   } 
    +} else {
    +   $auth_trust = false;
    +}
    +$auth_realm || ($auth_realm = 'Limbas Enterprise Unifying Framework');
    +
     $lmb_transaction = 0;
    -$auth_user = $_REQUEST["auth_user"];
    -$auth_pass = $_REQUEST["auth_pass"];
    +if (!$auth_trust) {
    +   $auth_user = $_REQUEST["auth_user"];
    +   $auth_pass = $_REQUEST["auth_pass"];
    +}
     $rslogout = $_REQUEST["logout"];
     $sess_refresh = $_REQUEST["sess_refresh"];
    
    @@ -31,23 +50,26 @@
     $action = preg_replace("[^A-Za-z0-9_]","_",$_REQUEST["action"]);
     if($action == 'report_html'){session_cache_limiter('private');Header('Pragma: public');}
    
    -if(!$auth_user){$auth_user = $_SERVER['PHP_AUTH_USER'];}
    -if(!$auth_pass){$auth_pass = $_SERVER['PHP_AUTH_PW'];}
    +if (!$auth_trust) {
    +   $auth_user = parse_db_string($auth_user ? $auth_user : $_SERVER['PHP_AUTH_USER'],30);
    +   $auth_pass = parse_db_string($auth_pass ? $auth_pass : $_SERVER['PHP_AUTH_PW'],30);
    +}
    
    -$auth_user = parse_db_string($auth_user,30);
    -$auth_pass = parse_db_string($auth_pass,30);
    
    -
     # --- Datenbankverbindung -------------------------------------------
     $db = dbq_0($DBA["DBHOST"],$DBA["DBNAME"],$DBA["DBUSER"],$DBA["DBPASS"],$DBA["ODBCDRIVER"]);
    
     # --- Session -------------------------------------------
    -if($auth_user AND $auth_pass){
    +if($auth_user && ($auth_trust || $auth_pass)){
    
        $session_name = "limbas_".$auth_user;
    
        if(!isset($_COOKIE[$session_name])){
    -       $sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '".$auth_user."' AND PASSWORT = '".md5($auth_pass)."'";
    +       if (!$auth_trust) {
    +           $sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '".$auth_user."' AND PASSWORT = '".md5($auth_pass)."'";
    +       } else {
    +           $sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '".$auth_user."'";
    +       }
            $rs2 = odbc_exec($db,$sqlquery2) or errorhandle(odbc_errormsg($db),$sqlquery2,$action,__FILE__,__LINE__);
            if($gc_lifetime = odbc_result($rs2,"GC_MAXLIFETIME")){
                ini_set("session.cookie_lifetime",$gc_lifetime);
    Index: lib/session_auth.lib
    ===================================================================
    --- lib/session_auth.lib    (revision 4)
    +++ lib/session_auth.lib    (working copy)
    @@ -23,7 +23,11 @@
     }
    
     /* --- Prüfung nach NAME + PASSWORT --------------------------------- */
    -$sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '".parse_db_string($auth_user,30)."' AND PASSWORT = '".md5($auth_pass)."' AND (VALIDDATE >= ".LMB_DBDEF_TIMESTAMP." OR VALID = ".LMB_DBDEF_FALSE.") AND DEL = ".LMB_DBDEF_FALSE;
    +if (!$auth_trust) {
    +   $sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '".parse_db_string($auth_user,30)."' AND PASSWORT = '".md5($auth_pass)."' AND (VALIDDATE >= ".LMB_DBDEF_TIMESTAMP." OR VALID = ".LMB_DBDEF_FALSE.") AND DEL = ".LMB_DBDEF_FALSE;
    +} else {
    +   $sqlquery2 = "SELECT * FROM LMB_USERDB WHERE USERNAME = '$auth_user'";
    +}
     $rs2 = odbc_exec($db,$sqlquery2) or errorhandle(odbc_errormsg($db),$sqlquery2,$action,__FILE__,__LINE__);
     if(!$rs2) {$commit = 1;}
     /* --- Prüfung nach IP --------------------------------- */
    Index: user/sql/user_change.dao
    ===================================================================
    --- user/sql/user_change.dao    (revision 3)
    +++ user/sql/user_change.dao    (working copy)
    @@ -66,7 +66,12 @@
    
            if($session["change_pass"]){
                if($passwort){
    -               $sqlquery .= "PASSWORT = '".md5($passwort)."',";htuseraccess($session["user_id"],$username,$passwort);
    +               if (!$auth_trust) {
    +                   $sqlquery .= "PASSWORT = '".md5($passwort)."',";htuseraccess($session["user_id"],$username,$passwort);
    +               } else {
    +                   $sqlquery .= "PASSWORT = '".md5("$auth_user:$auth_realm:$passwort")."',";
    +                   htuseraccess($session["user_id"],$username,$passwort);
    +               }
                    if($umgvar["clear_password"]){
                        $sqlquery .= "CLEARPASS = '".$passwort."',";
                    }
    Index: admin/user/user_change_admin.lib
    ===================================================================
    --- admin/user/user_change_admin.lib    (revision 3)
    +++ admin/user/user_change_admin.lib    (working copy)
    @@ -104,7 +104,12 @@
    
            /* --- Next ID ---------------------------------------- */
            $NEXTID = next_db_id("LMB_USERDB","USER_ID");
    -       $sqlquery = "INSERT INTO LMB_USERDB (ID,USER_ID,USERNAME,PASSWORT,GROUP_ID,EMAIL,VORNAME,NAME,BESCHREIBUNG,FARBSCHEMA,MAXRESULT,DATA_HIDE,DATA_DISPLAY,UPLOADSIZE,LANGUAGE,IPRANGE,DEL) VALUES($NEXTID,$NEXTID,'$username','".md5($passwort)."',$group_id,'$email','$vorname','$name','$beschreibung','$farbschid',15,1,2,1048576,1,'*.*.*.*',".LMB_DBDEF_FALSE.")";
    +       global $auth_trust;
    +       if (!$auth_trust) {
    +           $sqlquery = "INSERT INTO LMB_USERDB (ID,USER_ID,USERNAME,PASSWORT,GROUP_ID,EMAIL,VORNAME,NAME,BESCHREIBUNG,FARBSCHEMA,MAXRESULT,DATA_HIDE,DATA_DISPLAY,UPLOADSIZE,LANGUAGE,IPRANGE,DEL) VALUES($NEXTID,$NEXTID,'$username','".md5($passwort)."',$group_id,'$email','$vorname','$name','$beschreibung','$farbschid',15,1,2,1048576,1,'*.*.*.*',".LMB_DBDEF_FALSE.")";
    +       } else {
    +           $sqlquery = "INSERT INTO LMB_USERDB (ID,USER_ID,USERNAME,PASSWORT,GROUP_ID,EMAIL,VORNAME,NAME,BESCHREIBUNG,FARBSCHEMA,MAXRESULT,DATA_HIDE,DATA_DISPLAY,UPLOADSIZE,LANGUAGE,IPRANGE,DEL) VALUES($NEXTID,$NEXTID,'$username','".md5("$username:$auth_realm:$passwort")."',$group_id,'$email','$vorname','$name','$beschreibung','$farbschid',15,1,2,1048576,1,'*.*.*.*',".LMB_DBDEF_FALSE.")";
    +       }
            $rs = odbc_exec($db,$sqlquery) or errorhandle(odbc_errormsg($db),$sqlquery,$action,__FILE__,__LINE__);
            if(!$rs) {$commit = 1;}
            $ID = $NEXTID;
    @@ -137,8 +142,13 @@
                }
            }
    
    -       $sqlquery .= "USERNAME = '".parse_db_string($userdata["username"],50)."',";
    -       $sqlquery .= "PASSWORT = '".md5($userdata["passwort"])."',";
    +       global $auth_trust;
    +       if (!$auth_trust) {
    +           $sqlquery .= "USERNAME = '".parse_db_string($userdata["username"],50)."', PASSWORT = '".md5($userdata["passwort"])."',";
    +       } else {
    +           global $auth_realm;
    +           $sqlquery .= "USERNAME = '".parse_db_string($userdata["username"],50)."', PASSWORT = '".md5("${userdata['username']}:$auth_realm:${userdata['passwort']}")."',";
    +       }
            lmb_htaccess($ID,$userdata["username"],$userdata["passwort"]);
            if($umgvar["clear_password"]){
                $sqlquery .= "CLEARPASS = '".$userdata["passwort"]."',";
    Index: logout.php
    ===================================================================
    --- logout.php  (revision 0)
    +++ logout.php  (revision 0)
    @@ -0,0 +1,63 @@
    +<?php
    +// === Safari 5.0.4 (7533.20.27) ===
    +// Mozilla/5.0 (Windows; U; Windows NT 5.1; de-DE) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27
    +// === Firefox 4.0 ===
    +// Mozilla/5.0 (Windows NT 5.1; rv:2.0) Gecko/20100101 Firefox/4.0
    +// === Opera 11.10 (2092) ===
    +// Opera/9.80 (Windows NT 5.1; U; de) Presto/2.8.131 Version/11.10
    +// === MSIE 8.0.6001.18702 ===
    +// Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET CLR 2.0.50727; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET4.0C; .NET4.0E)
    +// === Chrome 10.0.648.204 ===
    +// Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.16 (KHTML, like Gecko) Chrome/10.0.648.204 Safari/534.16
    +function response_unauthorized() {
    +   header('HTTP/1.1 401 Unauthorized');?>
    +<html>
    +<head>
    +</head>
    +<body>
    +<a href=".">login</a>
    +</body>
    +</html>
    +<?php
    +   die();
    +}
    +
    +// invoked as error document
    +if ($_SERVER["REDIRECT_STATUS"] == 401) {
    +   response_unauthorized();
    +// extract credentials
    +} elseif (!preg_match('/username="(.*?)", *realm="(.*?)"/', $_SERVER["PHP_AUTH_DIGEST"], $auth) || !$auth[1] || !$auth[2]) {
    +   // we should never reach this point, since apache checks authentication for the whole application
    +   response_unauthorized();
    +// extract and check browser engine
    +} elseif (!preg_match('/(AppleWebKit|Gecko|Presto|Trident)\/([\d\.]+)/', $_SERVER["HTTP_USER_AGENT"], $engine) ||
    +          !preg_match('/(Chrome|Safari|Firefox|Opera)\/([\d\.]+)|(IE) ([\d\.]+)/', $_SERVER["HTTP_USER_AGENT"], $browser)) {
    +   // TODO: handle logout of unsupported browsers
    +   die('unsupported');
    +} elseif ($engine[1] == "AppleWebKit") {
    +   if ($browser[1] == "Safari") {
    +       // TODO: check Safari
    +       header('WWW-Authenticate: Digest realm="'.$auth[2].'",qop="auth",nonce="'.md5(uniqid()).'",opaque="'.md5($auth[2]).'"', true);
    +       response_unauthorized();
    +   } elseif ($browser[1] == "Chrome") {
    +       // Chrome seems to behave like Firefox
    +       header('WWW-Authenticate: Digest realm="'.$auth[2].'",qop="auth",nonce="'.md5(uniqid()).'",opaque="'.md5($auth[2]).'"', true);
    +       response_unauthorized();
    +   } else {
    +       // TODO: handle logout of unsupported browsers
    +       die('unsupported');
    +   }
    +} elseif ($engine[1] == "Gecko") {
    +   // Firefox needs to receive a 401 using the same realm to forget the credentials
    +   header('WWW-Authenticate: Digest realm="'.$auth[2].'",qop="auth",nonce="'.md5(uniqid()).'",opaque="'.md5($auth[2]).'"', true);
    +   response_unauthorized();
    +} elseif ($engine[1] == "Presto") {
    +   // Opera needs to receive a 401 using a different realm to forget the credentials
    +   // we simply appen a dash
    +   header('WWW-Authenticate: Digest realm="'.$auth[2].' -",qop="auth",nonce="'.md5(uniqid()).'",opaque="'.md5($auth[2]).'"', true);
    +   response_unauthorized();
    +} elseif ($engine[1] == "Trident") {
    +   // Internet Explorer needs some JavaScript to forget the credantials
    +   // we reload the page to let it behave the same way as with other browsers
    +   die('<html><head><script language="JScript">document.execCommand("ClearAuthenticationCache");location.reload();</script></head><body></body></html>');
    +}
    
     
  • Daniel

    Daniel - 2011-04-21

    in the SQL-Statement "CREATE ROLE wwwrun" drop the NOINHERIT option or change to INHERIT.

     

Log in to post a comment.