1<?php 2/** 3 * DokuWiki Plugin authclientcert (Auth Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Pawel Jasinski <pawel.jasinski@gmail.com> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) { 11 die(); 12} 13 14class auth_plugin_authclientcert extends auth_plugin_authplain 15{ 16 17 /** 18 * Constructor 19 */ 20 public function __construct() { 21 parent::__construct(); // for compatibility 22 $this->cando['addUser'] = false; // can Users be created? 23 $this->cando['delUser'] = true; // can Users be deleted? 24 $this->cando['modLogin'] = false; // can login names be changed? 25 $this->cando['modPass'] = false; // can passwords be changed? 26 $this->cando['modName'] = false; // can real names be changed? 27 $this->cando['modMail'] = false; // can emails be changed? 28 $this->cando['modGroups'] = true; // can groups be changed? 29 $this->cando['getGroups'] = true; // can a list of available groups be retrieved? 30 $this->cando['external'] = true; // does the module do external auth checking? 31 $this->cando['logout'] = true; // possible for user logged in with password 32 } 33 34 /** 35 * Do all authentication [ OPTIONAL ] 36 * 37 * @param string $user Username 38 * @param string $pass Cleartext Password 39 * @param bool $sticky Cookie should not expire 40 * 41 * @return bool true on successful auth 42 */ 43 public function trustExternal($user, $pass, $sticky=false) { 44 global $USERINFO; 45 $sticky ? $sticky = true : $sticky = false; //sanity check 46 47 // error_log("trustExternal of authremoteuser\n", 3, "/tmp/plugin.log"); 48 $header_name = $this->getConf('http_header_name'); 49 if (empty($header_name)) { 50 $this->_debug("CLIENT CERT: http_header_name is empty", 0, __LINE__, __FILE__); 51 return false; 52 } 53 $cert = $_SERVER[$header_name]; 54 if (empty($cert)) { 55 $this->_debug("CLIENT CERT: missing http header ($header_name)", 0, __LINE__, __FILE__); 56 return false; 57 } 58 $certUserInfo = $this->_extractUserInfoFromCert($cert); 59 msg(print_r($certUserInfo, true)); 60 if (empty($certUserInfo)) { 61 return false; 62 } 63 $remoteUser = $certUserInfo['user']; 64 $userinfo = $this->_upsertUser($certUserInfo); 65 if(empty($userinfo)) { 66 return false; 67 } 68 $_SERVER['REMOTE_USER'] = $remoteUser; 69 $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name'] = $userinfo['name']; 70 $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail'] = $userinfo['mail']; 71 $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps'] = $userinfo['grps']; 72 $_SESSION[DOKU_COOKIE]['auth']['info']['user'] = $remoteUser; 73 $_SESSION[DOKU_COOKIE]['auth']['user'] = $remoteUser; 74 $this->cando['logout'] = false; // not possible as long as certificate is provided 75 return true; 76 } 77 78 protected function _upsertUser($certUserInfo) { 79 $user = $certUserInfo['user']; 80 $userInfo = $this->getUserData($user); 81 if ($userInfo !== false) { 82 // modify user? 83 return $userInfo; 84 } 85 $group = $this->getConf('group'); 86 if (empty($group)) { 87 $group = "user"; 88 } 89 $group = $this->cleanGroup($group); 90 if ($this->createUser($user, auth_pwgen().auth_pwgen(), $certUserInfo['name'], $certUserInfo['mail'], array($group))) { 91 return $this->users[$user]; 92 } 93 $this->_debug("CLIENT CERT: Unable to autocreate user", 0, __LINE__, __FILE__); 94 return false; 95 } 96 97 protected function _formatCert($cert) { 98 // restore BEGIN/END CERTIFICATE if missing 99 $pattern = '/-----BEGIN CERTIFICATE-----(.*)-----END CERTIFICATE-----/msU'; 100 if (1 === preg_match($pattern, $cert, $matches)) { 101 $cert = $matches[1]; 102 $replaceCharacters = array(" ", "\t", "\n", "\r", "\0" , "\x0B"); 103 $cert = str_replace($replaceCharacters, '', $cert); 104 } 105 return "-----BEGIN CERTIFICATE-----".PHP_EOL.$cert.PHP_EOL."-----END CERTIFICATE-----".PHP_EOL; 106 } 107 108 protected function _extractUserInfoFromCert($cert) { 109 $cert = $this->_formatCert($cert); 110 if (empty($cert)) { 111 $this->_debug("CLIENT CERT: unable to locate user certificate", 0, __LINE__, __FILE__); 112 return false; 113 } 114 $_SESSION['SSL_CLIENT_CERT'] = $cert; 115 $client_cert_data = openssl_x509_parse($cert); 116 if (empty($client_cert_data)) { 117 $this->_debug("CLIENT CERT: unable to parse user certificate $client_cert_data", 0, __LINE__, __FILE__); 118 return false; 119 } 120 121 // this could be anything like: givenName sn, sn givenName, uid, ... 122 // [subject] => Array ( [C] => CH [O] => Admin [OU] => Array ( [0] => VBS [1] => V ) [UNDEF] => E1024143 [CN] => Pawel Jasinski ) 123 $name = $client_cert_data['subject']['CN']; 124 if (empty($name)) { 125 $this->_debugCert($client_cert_data, "CLIENT CERT: user certificate is missing subject.CN", 0, __LINE__, __FILE__); 126 return false; 127 } 128 129 // go after 2.16.840.1.113730.3.1.3 - employeeNumber 130 // [name] => /C=CH/O=Admin/OU=VBS/OU=V/2.16.840.1.113730.3.1.3=E1024143/CN=Pawel Jasinski 131 $cert_name = $client_cert_data['name']; 132 $employee_number = $this->_getOID("2.16.840.1.113730.3.1.3", $cert_name); 133 if (empty($employee_number)) { 134 $this->_debugCert($client_cert_data, "CLIENT CERT: user certificate is missing user name (employee number)", 0, __LINE__, __FILE__); 135 return false; 136 } 137 // go after email address in extension.subjectAltName 138 // [extensions] => Array ( [subjectAltName] => email:Pawel.Jasinski@vtg.admin.ch, othername: ...<snip/> 139 $altName = $client_cert_data['extensions']['subjectAltName']; 140 $mail = null; 141 foreach (explode(",", $altName) as $part) { 142 $nameval = explode(":", $part, 2); 143 if (count($nameval) == 2 && $nameval[0] == "email") { 144 $mail = trim($nameval[1]); 145 break; 146 } 147 } 148 if (empty($mail)) { 149 $this->_debugCert($client_cert_data, "CLIENT CERT: user certificate is missing email address", 0, __LINE__, __FILE__); 150 return false; 151 } 152 $user = $this->cleanUser($employee_number); 153 return ['name' => $name, 'mail' => $mail, 'user' => $user ]; 154 } 155 156 private function _getOID($OID, $name) { 157 preg_match('/\/' . $OID . '=([^\/]+)/', $name, $matches); 158 return $matches[1]; 159 } 160 161 /** 162 * Wrapper around msg() but outputs only when debug is enabled 163 * 164 * @param string $message 165 * @param int $err 166 * @param int $line 167 * @param string $file 168 * @return void 169 */ 170 protected function _debug($message, $err, $line, $file) { 171 if(!$this->getConf('debug')) return; 172 msg($message, $err, $line, $file); 173 } 174 175 protected function _debugCert($client_cert_data, $message, $err, $line, $file) { 176 $cert_dump = print_r($client_cert_data, true); 177 $this->_debug($message." ".$client_cert_data.$cert_dump, $err, $line, $file); 178 } 179} 180 181