Calls an external application and passes the password to it on standard input.
author: Zbigniew Jędrzejewski-Szmek
lastupdate : 2009-11-07
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:
www ALL = (ALL) NOPASSWD: /usr/bin/python /usr/local/bin/pamlogin.py
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.
<?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 ); } }
Requires simplejson and python-pam (the module is called PAM). Not to be confused with a different module based on ctypes called pam.
#!/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())
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.
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.