Table of Contents

The htaccess class

UPDATED TO VERSION 1.2 — 5 June, 2006:
See Changelog for new features and fixed bugs.

:!: If you upgrade from version 1.0, you have also to upgrade the format of your htusers.auth.php file.

In latest Dokuwiki, where wiki:auth:backends are php classes, the first wiki:tips:htaccessauth method doesn't work.

My solution is based on both the first revision of this page and the wiki:auth:plain authentication backend, and it's fully tested on 2006-03-09 DokuWiki version.

The wiki:acl feature is supported and User and Group Management is the same as in wiki:auth:plain authentication backend, except the fact that less user informations are needed, because they can be retrieved directly from your Web Server.

Installation

Unpack the htaccessauth.zip in your top dokuwiki folder or manually copy/paste the htaccess.class.php code. Follow instructions in htusers.auth.php and Configuration.

htaccess.class.php

Create a htaccess.class.php file in inc/auth/ directory with this contents:

<?php
/**
 * Htaccess authentication backend
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Samuele Tognini <samuele@netsons.org>
 * Version: 1.2
 * last modified: 2006-06-05 11:46:00
 *
 * Work based on the plaintext authentication backend:
 * @author     Andreas Gohr <andi@splitbrain.org>
 * @author     Chris Smith <chris@jalakai.co.uk>
 * 
 * and on the .htaccess authentication backed:
 * @author     Marcel Meulemans <marcel_AT_meulemans_DOT_org>
 * Additions:  Sebastian S <Seb.S@web.expr42.net>
 *
 */
 
define('DOKU_AUTH', dirname(__FILE__));
require_once(DOKU_AUTH.'/basic.class.php');
 
define('AUTH_USERFILE',DOKU_CONF.'htusers.auth.php');
 
if(isset($_REQUEST['u']))
     $_REQUEST['u'] = cleanID($_REQUEST['u']);
     if(isset($_REQUEST['acl_user']))
     $_REQUEST['acl_user'] = cleanID($_REQUEST['acl_user']);
 
     class auth_htaccess extends auth_basic {
 
       var $users = null;
       var $_pattern = array();
 
       /**
	* Constructor
	* 
	* Carry out sanity checks to ensure the object is
	* able to operate. Set capabilities.
	*
	* @author Samuele Tognini <samuele@netsons.org>
	*/
       function auth_htaccess() {
	 global $conf;
	 if (!@is_readable(AUTH_USERFILE)){
	   $this->success = false;
	 }else{
	   if(@is_writable(AUTH_USERFILE)){
	     $this->cando['addUser']      = true;
	     $this->cando['delUser']      = true;
	     $this->cando['modName']      = true;
	     $this->cando['modMail']      = true;
	     $this->cando['modLogin']     = true;
	     $this->cando['modGroups']    = true;
	   }
	   $this->cando['getGroups']    = true;
	   $this->cando['getUsers']     = true;
	   $this->cando['getUserCount'] = true;
	   $this->cando['logoff'] = true;
	 }
       }
 
 
       /**
	* Check user+password [required auth function]
	*
	* Checks if the given user exists and the given
	* plaintext password is correct
	*
	* @author  Andreas Gohr <andi@splitbrain.org>
	* Modified by Samuele Tognini <samuele@netsons.org>
	* @return  bool
	*/
       function checkPass($user='',$pass=''){
	 global $conf;
	 //To test. it should work on all enviroment, more than PHP_AUTH_USER.
	 //change also the local.conf.
	 //if (isset($_SERVER['REMOTE_USER'])) {
	 if(isset($_SERVER['PHP_AUTH_USER']) and isset($_SERVER['PHP_AUTH_PW'])) {
	   $userinfo = $this->getUserData($_SERVER['PHP_AUTH_USER']);
	   if ($userinfo === false) return false;
	   return true;
	 }else{
	   return false;
	 }
 
       }
 
       /**
	* Logoff user
	*
	* @author  Samuele Tognini <samuele@netsons.org>
	*
	*/
       function logOff(){
	 global $conf;
	 //works only with basic http authentication
	 if (isset($conf['htaccess_realm'])) {
	   $default_msg="Successful logout. Retry login <a href='".DOKU_BASE."'>here</a>.";
	   header('WWW-Authenticate: Basic realm="'.$conf['htaccess_realm'].'"');
	   header('HTTP/1.0 401 Unauthorized');
	   (isset($conf['htaccess_logout'])) ? print($conf['htaccess_logout']) : print($default_msg);
	   exit;
	 } else {
	   //Workaround for unauthorized users.
	   (isset($conf['htaccess_unauthurl'])) ?  $url=$conf['htaccess_unauthurl'] : $url="..";
	   //True logout requested
	   if ($this->checkPass('','')) $url=DOKU_BASE;
	   header('Location:'.$url);
	 }
       }      
 
       /**
	* Return user info
	*
	* Returns info about the given user needs to contain
	* at least these fields:
	*
	* name string  full name of the user
	* mail string  email addres of the user
	* grps array   list of groups the user is in
	*
	* @author  Andreas Gohr <andi@splitbrain.org>
	* Modified by Samuele Tognini <samuele@netsons.org>
	*/
       function getUserData($user,$ht_defaultgrp=true){
	 global $conf;
	 if($this->users === null) $this->_loadUserData();
 
	 //Every user gets the default group
	 if (isset($conf['htaccess_defaultgrp']) && $ht_defaultgrp) {
	   $grps=$conf['htaccess_defaultgrp'];
	   //Also user not in auth file gets a valid dokuwiki login
	   if (!isset($this->users[$user])) {
	     $name=$this->_get_posixname($user);
	     (isset($conf['htaccess_domain'])) ? $mail = $user."@".$conf['htaccess_domain'] : $mail='';
	     $grps=array($grps);
	     $this->users[$user] = compact('name','mail','grps');
	   }
	   //htaccess_defaultgroup in first position
	   (empty($this->users[$user]['grps'][0])) ? $this->users[$user]['grps'][0] = $grps : array_unshift($this->users[$user]['grps'],$grps); 
	 }
	 return isset($this->users[$user]) ? $this->users[$user] : false;
       }
 
       /**
	*  Remove one or more users from the list of registered users
	*
	*  @author  Christopher Smith <chris@jalakai.co.uk>
	*  @param   array  $users   array of users to be deleted
	*  @return  int             the number of users deleted
	*/
       function deleteUsers($users) {
 
	 if (!is_array($users) || empty($users)) return 0;
 
	 if ($this->users === null) $this->_loadUserData();
 
	 $deleted = array();
	 foreach ($users as $user) {
	   if (isset($this->users[$user])) $deleted[] = preg_quote($user,'/');
	 }
 
	 if (empty($deleted)) return 0;
 
	 $pattern = '/^('.join('|',$deleted).'):/';
 
	 if (io_deleteFromFile(AUTH_USERFILE,$pattern,true)) {
	   foreach ($deleted as $user) unset($this->users[$user]);
	   return count($deleted);
	 }
 
	 // problem deleting, reload the user list and count the difference
	 $count = count($this->users());
	 $this->_loadUserData();
	 $count -= $count($this->users());
	 return $count;
       }
 
       /**
	* Create a new User
	*
	* Returns false if the user already exists, null when an error
	* occured and true if everything went well.
	*
	* The new user will be added to the default group by this
	* function if grps are not specified (default behaviour).
	*
	* @author  Andreas Gohr <andi@splitbrain.org>
	* @author  Chris Smith <chris@jalakai.co.uk>
	* Modified by Samuele Tognini <samuele@netsons.org>
	*/
       function createUser($user,$pwd,$name,$mail,$grps=null){
	 global $conf;
 
	 // user mustn't already exist in auth file
	 if ($this->getUserData($user,false) !== false) {
	   msg("User $user already exists.",-1);
	   return false;
	 }
 
	 // set default group if no groups specified
	 if (!is_array($grps)) $grps = array($conf['defaultgroup']);
 
	 // prepare user line
	 if ($mail == $user."@".$conf['htaccess_domain']) $mail = '';
	 $groups = join(',',$grps);
 
	 $userline = join(':',array($user,$name,$mail,$groups))."\n";
	 if (io_saveFile(AUTH_USERFILE,$userline,true)) {
	   //only for first visualization
	   $name=$this->_get_posixname($user);      
	   $this->users[$user] = compact('name','mail','grps');
	   if (isset($conf['htaccess_defaultgrp']))
	     msg("User inherits the default htaccess group: ".$conf['htaccess_defaultgrp'],0);
	   return true;
	 }
 
	 msg('The '.AUTH_USERFILE.' file is not writable. Please inform the Wiki-Admin',-1);
	 return null;
       }
 
       /**
	* Modify user data 
	* 
	* @author  Chris Smith <chris@jalakai.co.uk>
	* Modified by Samuele Tognini <samuele@netsons.org>
	* @param   $user      nick of the user to be changed
	* @param   $changes   array of field/value pairs to be changed (password will be clear text)
	* @return  bool
	*/
       function modifyUser($user, $changes) {
	 global $conf;
	 global $ACT;
	 global $INFO;
 
	 // sanity checks, user must already exist in auth file and there must be something to change
	 if (($userinfo = $this->getUserData($user,false)) === false) return false;
	 if (!is_array($changes) || !count($changes)) return true;
	 // update userinfo with new data
	 $newuser = $user;
	 foreach ($changes as $field => $value) {
	   if ($field == 'user') {
	     $newuser = $value;
	     continue;
	   }
	   $userinfo[$field] = $value;
	 }
 
	 //htaccess_defaultgrp first occurrence isn't added
	 $a=array_search($conf['htaccess_defaultgrp'],$userinfo['grps']);
	 if (is_numeric($a)) unset($userinfo['grps'][$a]);
 
	 $groups = join(',',$userinfo['grps']);     
	 $newname= $userinfo['name'];
	 $newmail=$userinfo['mail'];
	 //User description is the same of system user. Write a blank field.
	 if ($newname==$this->_get_posixname($newuser)) $newname='';
	 //Standard mail.Write a blank field.
	 if (isset($conf['htaccess_domain']) && $newmail==$newuser."@".$conf['htaccess_domain']) $newmail='';
 
	 $userline = join(':',array($newuser, $newname, $newmail, $groups))."\n";
 
	 if (!$this->deleteUsers(array($user))) {
	   msg('Unable to modify user data. Please inform the Wiki-Admin',-1);
	   return false;
	 }
 
	 if (!io_saveFile(AUTH_USERFILE,$userline,true)) {
	   msg('There was an error modifying your user data. You should register again.',-1);
	   // FIXME, user has been deleted but not recreated, should force a logout and redirect to login page
	   $ACT == 'register';
	   return false;
	 }
 
	 $this->users[$newuser] = $userinfo;
	 if (isset($conf['htaccess_defaultgrp']))
	   msg("User inherits the default htaccess group: ".$conf['htaccess_defaultgrp'],1);
	 return true;
       }
 
       /**
	* Return a count of the number of user which meet $filter criteria
	*
	* @author  Chris Smith <chris@jalakai.co.uk>
	*/
       function getUserCount($filter=array()) {
 
	 if($this->users === null) $this->_loadUserData();
 
	 if (!count($filter)) return count($this->users);
 
	 $count = 0;
	 $this->_constructPattern($filter);
 
	 foreach ($this->users as $user => $info) {
	   $count += $this->_filter($user, $info);
	 }
 
	 return $count;
       }
 
       /**
	* Bulk retrieval of user data
	*
	* @author  Chris Smith <chris@jalakai.co.uk>
	* @param   start     index of first user to be returned
	* @param   limit     max number of users to be returned
	* @param   filter    array of field/pattern pairs
	* @return  array of userinfo (refer getUserData for internal userinfo details)
	*/
       function retrieveUsers($start=0,$limit=0,$filter=array()) {
	 global $conf;
	 $grps=$conf['htaccess_defaultgrp'];
	 if ($this->users === null || !empty($grps)) 
	   $this->_loadUserData();
 
	 ksort($this->users);
 
	 $i = 0;
	 $count = 0;
	 $out = array();
	 $this->_constructPattern($filter);
 
	 foreach ($this->users as $user => $info) {
	   //display the htaccess default group
	   if (!empty($grps))
	     (empty($info['grps'][0])) ? $info['grps'][0] = $grps : array_unshift($info['grps'],$grps); 
	   if ($this->_filter($user, $info)) {
	     if ($i >= $start) {
	       $out[$user] = $info;
	       $count++;
	       if (($limit > 0) && ($count >= $limit)) break;
	     }
	     $i++;
	   }
	 }
	 return $out;
       }
 
       /**
	* Return the user name information from system
	*
	* @author Samuele Tognini <samuele@netsons.org>
	*/
       function _get_posixname ($sys_user){
	 $sys_name='';
	 $sys_user= posix_getpwnam($sys_user);
	 if (@$sys_user) {
	   list($id, $pass, $uid, $gid, $extra) = array_values($sys_user);
	   list($username, $recycle) = split(",", $extra, 2);
	   $sys_name = urldecode($username);
	 }
	 return $sys_name;
       }
 
       /**
	* Load all user data
	*
	* loads the user file into a datastructure
	*
	* @author Samuele Tognini <samuele@netsons.org>
	*/
       function _loadUserData(){
	 global $conf;
	 $this->users = array();
 
	 if(!@file_exists(AUTH_USERFILE)) return;
 
	 $lines = file(AUTH_USERFILE);
 
	 foreach($lines as $line){
	   $line = preg_replace('/#.*$/','',$line); //ignore comments
	   $line = trim($line);
	   if(empty($line)) continue;
	   $row = split(":",$line,4);
	   if (isset($row[3])) {
	     $tmp_name = $row[1];
	     $tmp_mail = $row[2];
	     //i try to get some user info from system
	     if (empty($tmp_name)) {
	       if ($sys_user= posix_getpwnam($row[0])) {
		 list($id, $pass, $uid, $gid, $extra) = array_values($sys_user);
		 list($username, $recycle) = split(",", $extra, 2);
		 $tmp_name = $username;
	       }
	     }	
	     if (empty($tmp_mail) && isset($conf['htaccess_domain'])) $tmp_mail = $row[0]."@".$conf['htaccess_domain'];
	     $user_groups=$row[3];
	     $groups = split(",",$user_groups);
	     $this->users[$row[0]]['name'] = urldecode($tmp_name);
	     $this->users[$row[0]]['mail'] = $tmp_mail;
	     $this->users[$row[0]]['grps'] = $groups;
	   } else {
	     msg("Errors for user ".$row[0].": Check your ".AUTH_USERFILE,-1);
	     $this->users[$row[0]] = '';
	   }
	 }
       }
       /**
	* return 1 if $user + $info match $filter criteria, 0 otherwise
	*
	* @author   Chris Smith <chris@jalakai.co.uk>
	*/
       function _filter($user, $info) {
	 // FIXME
 
	 foreach ($this->_pattern as $item => $pattern) {
	   if ($item == 'user') {
	     if (!preg_match($pattern, $user)) return 0;
	   } else if ($item == 'grps') {
	     if (!count(preg_grep($pattern, $info['grps']))) return 0;
	   } else {
	     if (!preg_match($pattern, $info[$item])) return 0;
	   }
	 }
	 return 1;
       }
 
       function _constructPattern($filter) {
	 $this->_pattern = array();
	 foreach ($filter as $item => $pattern) {
	   //        $this->_pattern[$item] = '/'.preg_quote($pattern,"/").'/';          // don't allow regex characters
	   $this->_pattern[$item] = '/'.str_replace('/','\/',$pattern).'/';    // allow regex characters
	 }
       }
     }
 
//Setup VIM: ex: et ts=2 enc=utf-8 :

htusers.auth.php

Create a htusers.auth.php file in conf/ directory and write here your list of DokuWiki users, their groups and their optional full names and mail.

If you want use the User Manager plugin in order to manage users through web, give write permissions on file to web user:

#> chgrp www-data conf/htusers.auth.php
#> chmod 664 conf/htusers.auth.php

The file format is name:full_name:mail:groups:

Remeber: these users must have also Basic HTTP Authorization in .htaccess file or access will be denied anyway.

This is an example:

# htusers.auth.php
# <?php exit()?>
#
# Htaccess Userfile
#
# Format:
#
# user:full name:mail:groups,comma,separated
jack:The Admin::admins
allen:::editors,uploaders
neal::allen@hisdomain.net:editors

acl.auth.php

Copy the standard distribution file conf/acl.auth.php.dist to conf/acl.auth.php, and give write permission to Web service user if you want change ACL through web interface.

For example:

#> chgrp www-data conf/acl.auth.php
#> chmod 664 conf/acl.auth.php

Configuration

In order to use htaccessauth you must use these settings in your conf/local.php:

$conf['authtype']     = 'htaccess';
if (isset($_SERVER['PHP_AUTH_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['PHP_AUTH_USER'];             

Optional settings

These settings in conf/local.php make customizable the htaccess backend:

htaccess_defaultgrp

Don't confuse it with wiki:config#defaultgroup option. Every user which get Basic HTTP authorization, gains automatically the htaccess_defaultgrp dokuwiki group too.

In this way you don't need anymore to create users in htusers.auth.php, whereas users that are already in htusers.auth.php will get automatically the htaccess_defaultgrp group.

Moreover, you can use acl to give custom permission to htaccess_defaultgrp. The User Manager plugin works correctly (when you create,modify or display users, it automatically give them the 'htaccess_defaultgrp' group), but it doesn't display the users that aren't in htaccess.auth.php.

Note that htaccess_defaultgrp will not be really writed by user manager inside htusers.auth.php, so that, if later you unset the $conf['htaccess_defaultgrp'] config, they will lost the htaccess_defaultgrp group and also the dokuwiki access if they aren't in htusers.auth.php.

Thanks to Dominique Launay for this great idea.

Optional
default : not used
Example : $conf['“htaccess_defaultgrp”] = “guest”;

htaccess_domain

You can either set a custom mail for every user in htusers.auth.php mail field, or leave it empty and set the htaccess_domain, so that users mail becames automatically user@htaccess_domain.

Optional
default : not used
Example : $conf['htaccess_domain'] = “example.com”;

htaccess_realm

The basic http authentication realm.

This is the AuthName directive in Apache web server configuration. You can find its value also in the login popup dialog.

If it's set then users will be able to logoff from Basic Http authentication using the logout button, if not then logout button will redirect users to the wiki main page.

A wrong value makes the logoff unsuccessful.

Optional
default : not used
Example : $conf[“htaccess_realm”]=“Limited Site”

Note: In order to logoff, click on dokuwiki 'logout' button, then click on then 'cancel' button of the login popup dialog.

htaccess_logout

You can display a custom message to users that logout dokuwiki through the htaccess_realm option. Only raw html code is supported.

Optional
default : Successful logout. Retry login <a href=dokuwiki_dir>login</a>.
Example : $conf[“htaccess_logout”]=“This is my message to you.”

htaccess_unauthurl

This is a simple workaround to use when dokuwiki acl system denies access to users, that has authenticated themself successfully through the HTTP Basic authentication system. In this case, when htaccess_unauthurl value is also a denied page,a loop can be generated ,so pay attention to set it correctly.

Unuseful togheter with the htaccess_realm option.

Optional
default : .. (the directory upper dokuwiki, change it if it's a denied path)
Example : $conf[“htaccess_unauthurl”]=“http://www.mysite.com

Others good options in conf/local.phpare:

  $conf['useacl']       = 1;           // this enables the ACL feature
  $conf['openregister'] = 0;           // Since you are using Basic HTTP Registration, i think openregister will not work . 
  $conf['superuser']    = '@admin';    // admin group is superuser, choose a user or a group from you htuser.auth.file

local.php configuration example

$conf['authtype']     = 'htaccess';   
$conf['useacl']       = 1;
$conf['openregister'] = 0;
$conf['superuser']    = '@admin';
$conf['htaccess_defaultgrp'] = "guest";
$conf['htaccess_domain'] = "example.com";
 
if (isset($_SERVER['PHP_AUTH_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['PHP_AUTH_USER'];   

Changelog

Notes

The module as provided only works if you are using mod_php. It works fine without mod_php if you make the following changes:

if (isset($_SERVER['REMOTE_USER']) and !isset($_SESSION[$conf['title']]['auth']['info'])) $_REQUEST['u'] = $_SERVER['REMOTE_USER'];

And in the module you need this extra block at line 77 3)

elseif( isset($_SERVER['REMOTE_USER']) ) {
       $userinfo = $this->getUserData($_SERVER['REMOTE_USER']);
       if ($userinfo === false) return false;
       return true;
     }
Reading documentation, it seems that $_SERVER['REMOTE_USER'] can be used in place of $_SERVER['PHP_AUTH_USER'] even with php_mode. For now, i don't change code because it needs testing feedback. — Samuele Tognini 2006-06-05 10:50
1) , 2)
Optional
3)
At else statment of checkpass function