External

Calls an external application and passes the password to it on standard input.

author: Zbigniew Jędrzejewski-Szmek

lastupdate : 2009-11-07

Download and Installation

The reason for this plugins existence is the need to authenticate through PAM. However, I didn't want to give the privilege to read encrypted passwords to the user that the Apache web server (and DokuWiki) run as. The setup goes as this:

  1. external.class.php is installed in inc/auth and DokuWiki authentication is set to 'external'
  2. the user running the webserver (usually something like www) is given the ability to execute a second program with elevated privileges. I used a program written in python, i.e. a script, so a simple set-uid will not work. I had to resort to sudo. Put this in /etc/sudoers:
    www    ALL = (ALL) NOPASSWD: /usr/bin/python /usr/local/bin/pamlogin.py
  3. install a program to do the actual password verification. It must read two lines from stdin, the JSON encoded username and the JSON encoded password. The password and username have to be somehow escaped so e.g. newlines in the password don't mess things up. JSON is simple and standard.

Using not a Python script but a real binary would have the advantage of not having to use sudo. A simple

  chgrp root.www /path/to/binary
  chmod u+s,g+x,o= /path/to/binary

should be enough.

The support for PAM is very primitive – an echoed prompt is assumed to be a request for the username, an non-echoed prompt is assumed to be the request for a password. The user information is duplicated: in dokuwiki and in the /etc/group, /etc/shadow. The password stored in the dokuwiki user database is ignored, and the one from /etc/group or /etc/shadow is used. Nevertheless, users must be created in both places.

DokuWiki plugin code: external.class.php

external.class.php
<?php                                                                      
/**                                                                        
 * external authentication backend                                         
 * @author Zbigniew Jędrzejewski-Szmek <zbyszek@in.waw.pl>                 
 * based on work by Michael Gorven <michael003+dokuwiki@gmail.com>         
 * @license GPL2 http://www.gnu.org/licenses/gpl.html                      
 */                                                                        
 
define('DOKU_AUTH', dirname(__FILE__));
require_once(DOKU_AUTH.'/plain.class.php');
 
class auth_external extends auth_plain
{                                     
    /**                               
     * Constructor                    
     *                                
     * Calls the auth_plain constructor which sets the backend's capabilities.
     * The change password capability is then removed since we can't change   
     * passwords through PAM without knowing the current password.            
     *                                                                        
     */                                                                       
    function auth_pam()                                                       
    {                                                                         
        // Call parent constructor                                            
        if (method_exists($this, 'auth_plain'))                               
            parent::auth_plain();                                             
 
        // Remove change password capability
        $this->cando['modPass'] = false;    
    }                                       
 
    /**
     * Checks the provided username and password using PAM.
     *                                                     
     * @param   string  $user   Username                   
     * @param   string  $pass   Password                   
     * @return  boolean True if authentication is successful
     */                                                     
    function checkPass( $user, $pass )                      
    {                                                       
        // Check that user exists                           
        if ( parent::getUserData($user) === false )         
            return false;                                   
 
        $userenc = json_encode($user) . "\n";
        $passenc = json_encode($pass) . "\n";
 
        $external_cmd = 'sudo /usr/bin/python /usr/local/bin/pamlogin.py';
 
        // Check password
        $handle = popen($external_cmd, 'w');
        fwrite($handle, $userenc);          
        fwrite($handle, $passenc);          
        $ret = pclose($handle);
 
        if($ret)
            return false;
 
        // Authentication succeded
        return true;
    }
 
    /**
     * Creates a new user.
     *
     * Uses the createUser() method in auth_plain to actually add the user.
     * This only adds the user to the list of DokuWiki users -- they must
     * separately be added to PAM and assigned a password.
     *
     * @param   string  $user   Username
     * @param   string  $pass   Password (will be blank)
     * @param   string  $name   Full name
     * @param   string  $mail   Email address
     * @param   string  $grps   List of groups
     * @return  bool    False if user already exists, null if error occurred, true if successful
     */
    function createUser($user,$pwd,$name,$mail,$grps=null)
    {
        // createUser() returns the password if successful. We therefore need to
        // set it to something non-empty otherwise it gets treated as false
        return parent::createUser( $user, time(), $name, $mail, $grps );
    }
 
}

Helper script: pamlogin.py

Requires simplejson and python-pam (the module is called PAM). Not to be confused with a different module based on ctypes called pam.

pamlogin.py
#!/usr/bin/python                                                                  
 
__version__ = '0.2'
 
import sys
import PAM
import errno
import simplejson
 
username = ''
password = ''
service = 'passwd'
 
def pam_conv(auth, query_list, userData=None):
        resp = []              
 
        for query,type in query_list:
                if type == PAM.PAM_PROMPT_ECHO_ON:
                        resp.append((username, 0))
                elif type == PAM.PAM_PROMPT_ECHO_OFF:
                        resp.append((password, 0))   
                elif type == PAM.PAM_PROMPT_ERROR_MSG or type == PAM.PAM_PROMPT_TEXT_INFO:
                        print >>sys.stderr, 'pamlogin: query (%s)' % query                
                        resp.append(('', 0));                                             
                else:                                                                     
                        return None                                                       
 
        return resp
 
def authenticate():
    auth = PAM.pam()
    auth.start(service)
    auth.set_item(PAM.PAM_USER, username)
    auth.set_item(PAM.PAM_CONV, pam_conv)
    try:
        auth.authenticate()
        auth.acct_mgmt()
    except PAM.error, (resp, code):
        print >>sys.stderr, 'pamlogin: auth failed (%s)' % resp
        return False
 
    return True
 
 
def read_user_pass():
    userenc = raw_input()
    passenc = raw_input()
    global username, password
    username = simplejson.loads(userenc)
    password = simplejson.loads(passenc)
 
def main():
    read_user_pass()
    if authenticate():
        return 0
    else:
        return errno.EPERM
 
if __name__ == '__main__':
    sys.exit(main())
Changelog

Troubleshooting

General

Become the user running the webserver and call the authentication program. Give JSON encoded username into its input (with extra quotes coming from JSON):

www$ sudo python /path/to/pamlogin.py && echo OK 
"userlogin"
"password"

If you don't see OK, then check syslog for messages.

python: pam_unix(passwd:auth): conversation failed

The error string 'conversation failed' means a problem between the Python script and PAM system. Not being able to open /etc/shadow or some other problem with permissions gives a different error.