1<?php
2
3/**
4 * CAS authentication plugin
5 *
6 * @licence   GPL 2 (http://www.gnu.org/licenses/gpl.html)
7 * @author    Xylle, Fabian Bircher
8 * @version   0.0.3
9 *
10 */
11
12use AuthSSOCas\SimpleFileLogger;
13use dokuwiki\Extension\AuthPlugin;
14use dokuwiki\Logger;
15
16
17class auth_plugin_authssocas extends AuthPlugin
18{
19    /**
20     * @var array|mixed
21     */
22    private array $options = array();
23
24    private ?string $logfileuser = null;
25
26    public function __construct()
27    {
28        global $conf;
29        parent::__construct();
30        require_once __DIR__ . '/vendor/autoload.php';
31
32        // Vérifie si la classe phpCAS existe
33        if (!class_exists('phpCAS')) {
34            msg("CAS err: phpCAS class not found.", -1);
35            $this->success = false;
36            return;
37        }
38        // Vérifie si l'extension curl existe
39        if (!extension_loaded("curl")) {
40            msg("CAS err: CURL php extension not found.", -1);
41            $this->success = false;
42            return;
43        }
44        // Définition des capacités de l'extension d'authentification
45        $this->cando['external'] = true;
46
47        // Création d'un journal des connexions, si un fichier est défini.
48        if ($this->getConf('logfileuser')) {
49            $this->logfileuser = $conf['logdir'] . "/" . $this->getConf('logfileuser');
50        }
51        if (!is_null($this->logfileuser) and !@is_readable($this->logfileuser)) {
52            if (!fopen($this->logfileuser, 'a')) {
53                msg("plainCAS: The CAS log users file could not be opened.", -1);
54                $this->success = false;
55            }
56        }
57
58
59        // Chargement des options
60        $this->options['debug'] = $this->getConf('debug');
61        $this->options['group_attribut'] = $this->getConf('group_attribut');
62        $this->options['group_attribut_separator'] = $this->getConf('group_attribut_separator');
63        $this->options['handlelogoutrequest'] = $this->getConf('handlelogoutrequest');
64        $this->options['handlelogoutrequestTrustedHosts'] = $this->getConf('handlelogoutrequestTrustedHosts');
65        $this->options['mail_attribut'] = $this->getConf('mail_attribut');
66        $this->options['name_attribut'] = $this->getConf('name_attribut');
67        $this->options['port'] = $this->getConf('port');
68        $this->options['samlValidate'] = $this->getConf('samlValidate');
69        $this->options['server'] = $this->getConf('server');
70        $this->options['rootcas'] = $this->getConf('rootcas');
71        $this->options['uid_attribut'] = $this->getConf('uid_attribut');
72        $this->options['cacert'] = $this->getConf('cacert');
73
74        $server_version = CAS_VERSION_2_0;
75        if ($this->getOption("samlValidate")) {
76            $server_version = SAML_VERSION_1_1;
77        }
78
79        if ($this->getOption("debug")) {
80            $logdir = $conf['logdir'];
81            $logger = new SimpleFileLogger($logdir . '/cas.log');
82            phpCAS::setLogger($logger);
83            phpCAS::setVerbose(true);
84        }
85
86        if (!DOKU_BASE == "/") {
87            $service_base_url = str_replace(DOKU_BASE, "", DOKU_URL);
88        } else {
89            $service_base_url = DOKU_URL;
90        }
91
92        // Configuration du client CAS
93        phpCAS::client(
94            $server_version,
95            $this->getOption('server'),
96            (int)$this->getOption('port'),
97            $this->getOption('rootcas'),
98            $service_base_url
99        );
100
101        if ($this->getConf('autologin')) {
102            phpCAS::setCacheTimesForAuthRecheck(-1);
103        } else {
104            phpCAS::setCacheTimesForAuthRecheck(1);
105        }
106
107        // Gestion de l'autorité de certification du certificat du serveur CAS pour la bibliothèque php_curl
108        $cas_cacert_file = DOKU_CONF . 'authssocas.cacert.pem';
109        if ($this->getOption('cacert')) {
110            if (!io_saveFile($cas_cacert_file, $this->getOption('cacert'))) {
111                msg('The ' . $cas_cacert_file . ' file is not writable. Please inform the Wiki-Admin', -1);
112            }
113            phpCAS::setCasServerCACert($cas_cacert_file);
114        } else {
115            phpCAS::setNoCasServerValidation();
116        }
117
118        // Gestion de la déconnexion sur le serveur CAS
119        if ($this->getOption('handlelogoutrequest')) {
120            phpCAS::handleLogoutRequests(true, $this->getOption('handlelogoutrequestTrustedHosts'));
121        } else {
122            phpCAS::handleLogoutRequests(false);
123        }
124    }
125
126    /**
127     *
128     * Récupère les options
129     *  Transforme en tableau les URL de notification de la déconnexion pour les serveurs CAS
130     *
131     * @param $optionName
132     * @return array|mixed|string[]|null
133     */
134    private function getOption($optionName): mixed
135    {
136        if (isset($this->options[$optionName])) {
137            switch ($optionName) {
138                case 'handlelogoutrequestTrustedHosts':
139                    $arr = explode(',', $this->options[$optionName]);
140                    foreach ($arr as $key => $item) {
141                        $arr[$key] = trim($item);
142                    }
143                    return $arr;
144                default:
145                    return $this->options[$optionName];
146            }
147        }
148        return NULL;
149    }
150
151    /**
152     *
153     * Transfert de la demande de connexion au serveur CAS
154     *
155     * @return void
156     * @noinspection PhpUnused
157     */
158    public function logIn(): void
159    {
160        global $ID;
161        $login_url = DOKU_URL . 'doku.php?id=' . $ID;
162
163        phpCAS::setFixedServiceURL($login_url);
164        phpCAS::forceAuthentication();
165    }
166
167    /**
168     *
169     * Déconnexion de l'utilisateur avec prise en compte de la déconnexion générale du CAS
170     *
171     * @return void
172     */
173    public function logOff(): void
174    {
175        global $ID;
176        global $USERINFO;
177
178        @session_start();
179        session_destroy();
180
181        $this->auth_log($USERINFO, "logout");
182        if ($this->getOption('handlelogoutrequest')) {
183            $logout_url = DOKU_URL . 'doku.php?id=' . $ID;
184            @phpCAS::logoutWithRedirectService($logout_url);
185        } else {
186            phpCAS::handleLogoutRequests();
187            unset($_SESSION);
188        }
189    }
190
191    public function trustExternal($user, $pass, $sticky = false): bool
192    {
193        global $USERINFO;
194
195        if (!empty($_SESSION[DOKU_COOKIE]['auth']['info'])) {
196            $USERINFO['name'] = $_SESSION[DOKU_COOKIE]['auth']['info']['name'];
197            $USERINFO['mail'] = $_SESSION[DOKU_COOKIE]['auth']['info']['mail'];
198            $USERINFO['grps'] = $_SESSION[DOKU_COOKIE]['auth']['info']['grps'];
199            $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user'];
200            return true;
201        }
202
203        if (phpCAS::isAuthenticated() or ($this->getOption('autologin') and phpCAS::checkAuthentication())) {
204
205            $USERINFO = $this->cas_user_attributes(phpCAS::getAttributes());
206            $this->auth_log($USERINFO, "login");
207            $_SESSION[DOKU_COOKIE]['auth']['user'] = $USERINFO['uid'];
208            $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
209            $_SERVER['REMOTE_USER'] = $USERINFO['uid'];
210            return true;
211        }
212
213        return false;
214    }
215
216    /**
217     *
218     * Renvoi les informations de l'utilisateur fournit par le CAS
219     *
220     * @param $attributes
221     * @return array
222     */
223    private function cas_user_attributes($attributes): array
224    {
225        return array(
226            'uid' => $attributes[$this->getOption('uid_attribut')] ?? '',
227            'name' => $attributes[$this->getOption('name_attribut')] ?? '',
228            'mail' => $attributes[$this->getOption('mail_attribut')] ?? '',
229            'grps' => $this->cas_user_groups($attributes),
230        );
231    }
232
233    /**
234     *
235     * Log user connection if the log file is defined
236     *
237     * format : DATE|TIME|ACTION|USER|CLIENT_IP|REAL_CLIENT_IP|USERINFO
238     * ACTION : login ou logout
239     * REAL_CLIENT_IP : si null, il y a un tiret
240     * USERINFO : si null, il y a un tiret
241     *
242     * @param $userinfo
243     * @param string $action
244     * @return void
245     */
246    private function auth_log($userinfo, string $action): void
247    {
248        if (!is_null($this->logfileuser)) {
249            $date = (new DateTime('now'))->format('Ymd|H:i:s');
250            $real_client_ip = ($this->getOption('http_header_real_ip') ? ($_SERVER[$this->getOption('http_header_real_ip')] ?? '-') : '-');
251            $client_ip = $_SERVER['REMOTE_ADDR'];
252
253            $utilisateur = $userinfo['uid'] ?? ($_SESSION[DOKU_COOKIE]['auth']['user'] ?? '-');
254            $informations = $userinfo ? json_encode($userinfo, JSON_UNESCAPED_UNICODE) : '-';
255
256            $userline = $date . "|" .
257                $action . "|" .
258                $utilisateur . '|' .
259                $client_ip . '|' .
260                $real_client_ip . '|' .
261                $informations .
262                PHP_EOL;
263
264
265            $this->write_log($userline);
266        }
267    }
268
269    /**
270     *
271     * Renvoi les groupes de l'utilisateur fournis par le CAS
272     * et s'assure que la valeur est bien de type array
273     *
274     * @param $attributes
275     * @return array
276     */
277    private function cas_user_groups($attributes): array
278    {
279        global $conf;
280        $raw_groups = $attributes[$this->getOption('group_attribut')] ?: array();
281        $user_groups = array();
282
283        Logger::debug("authssocas: raw user groups '" . implode(',', (array)$raw_groups) . "' - Group separator : '" . $this->getOption('group_attribut_separator') . "' - defaultgroup : '{$conf['defaultgroup']}'");
284        if (!$this->getOption('group_attribut_separator')) {
285            # Sans configuration de group_attribut_separator : la valeur retournée par CAS doit être un tableau.
286            if (!is_array($raw_groups)) {
287                $user_groups = array($raw_groups);
288            } else {
289                $user_groups = $raw_groups;
290            }
291        } else {
292            # Avec une configuration `group_attribut_separator` : la valeur retournée par CAS doit être une chaîne de caractères.
293            if (is_array($raw_groups)) {
294                $user_groups = $raw_groups;
295            } elseif (is_string($raw_groups)) {
296                $user_groups = explode($this->getOption('group_attribut_separator'), $raw_groups);
297            }
298        }
299
300        # Toujours ajouter le groupe par défaut (comme le font les autres plugins d'authentification).
301        if ($conf['defaultgroup'] && !in_array($conf['defaultgroup'], $user_groups)) {
302            $user_groups[] = $conf['defaultgroup'];
303        }
304        return $user_groups;
305    }
306
307    /**
308     * @param string $userline
309     * @return void
310     */
311    public function write_log(string $userline): void
312    {
313        if (!io_saveFile($this->logfileuser, $userline, true)) {
314            msg($this->getLang('writefail'), -1);
315        }
316    }
317}
318