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