1<?php 2 3use dokuwiki\Extension\Event; 4use JetBrains\PhpStorm\NoReturn; 5 6/** 7 * Provides generic SSO authentication 8 * @author Etienne MELEARD <etienne.meleard@renater.fr> 9 * @date 2018-08-27 10 */ 11 12class auth_plugin_genericsso extends DokuWiki_Auth_Plugin { 13 /** @var bool */ 14 public $success = false; 15 16 /** 17 * Possible things an auth backend module may be able to 18 * do. The things a backend can do need to be set to true 19 * in the constructor. 20 * 21 * @var array 22 */ 23 protected $cando = [ 24 'addUser' => false, // can Users be created? 25 'delUser' => false, // can Users be deleted? 26 'modLogin' => false, // can login names be changed? 27 'modPass' => false, // can passwords be changed? 28 'modName' => false, // can real names be changed? 29 'modMail' => false, // can emails be changed? 30 'modGroups' => false, // can groups be changed? 31 'getUsers' => false, // can a (filtered) list of users be retrieved? 32 'getUserCount' => false, // can the number of users be retrieved? 33 'getGroups' => false, // can a list of available groups be retrieved? 34 'external' => true, // does the module do external auth checking? 35 'logout' => true, // can the user logout again? (eg. not possible with HTTP auth) 36 ]; 37 38 private array|null $_conf = null; 39 private array|null $_attrs = null; 40 private array|null $_users = null; 41 42 /** 43 * Constructor 44 */ 45 public function __construct() { 46 parent::__construct(); 47 48 $this->_getConf(); // Checks config 49 50 $this->success = true; 51 } 52 53 /** 54 * Get config if valid 55 * 56 * @throw Exception 57 */ 58 private function _getConf(string $param = null): array|string|null { 59 if(!$this->_conf) { 60 global $conf; 61 $this->_conf = $conf['plugin']['genericsso']; 62 63 $bad = []; 64 foreach([ 65 'login_url', 'logout_url', 'home_url', 'headers', 'autologin', 66 'idp_attribute', 'id_attribute', 'email_attribute', 'fullname_attribute' 67 ] as $p) { 68 if(!array_key_exists($p, $this->_conf)) 69 $bad[] = $p; 70 } 71 72 if($bad) { 73 msg('Bad configuration for Dokuwiki SSO login : '.implode(', ', $bad), -1); 74 return null; 75 } 76 } 77 78 if($param) { 79 if(!array_key_exists($param, $this->_conf)) { 80 msg('Unknown configuration parameter for Dokuwiki SSO login : '.$param, -1); 81 return null; 82 } 83 84 return $this->_conf[$param]; 85 } 86 87 return $this->_conf; 88 } 89 90 /** 91 * Get attributes if any 92 * 93 * @throw Exception 94 */ 95 private function _getAttributes(bool $fatal = true, string $attr = null): array|null { 96 if(is_null($this->_attrs)) { 97 $headers = $this->_getConf('headers') ? getallheaders() : null; 98 99 $this->_attrs = []; 100 foreach(['idp', 'id', 'email', 'fullname'] as $k) { 101 $src = $this->_getConf($k.'_attribute'); 102 if($headers) { 103 $this->_attrs[$k] = array_key_exists($src, $headers) ? $headers[$src] : null; 104 } else { 105 $this->_attrs[$k] = getenv($src); 106 } 107 } 108 } 109 110 $bad = array_map(function($k) { 111 return $this->_getConf($k.'_attribute'); 112 }, array_keys(array_filter($this->_attrs, 'is_null'))); 113 114 if($bad && $fatal) { 115 msg('Missing attribute(s) for Dokuwiki SSO login : '.implode(', ', $bad), -1); 116 return []; 117 } 118 119 if($attr) { 120 $v = array_key_exists($attr, $this->_attrs) ? $this->_attrs[$attr] : null; 121 122 if($v) 123 return $v; 124 125 if($fatal) 126 msg('Missing attribute(s) for Dokuwiki SSO login : '.implode(', ', $bad), -1); 127 128 return null; 129 } 130 131 return $this->_attrs; 132 } 133 134 /** 135 * Check if any attributes 136 */ 137 private function _hasAttributes(string $attr = null): bool { 138 $attrs = $this->_getAttributes(false); 139 140 if(is_string($attr)) 141 return array_key_exists($attr, $attrs) && !is_null($attrs[$attr]); 142 143 return count(array_filter($this->_attrs)) > 0; 144 } 145 146 /** 147 * Log info 148 */ 149 private function _log(string $msg): void { 150 error_log('Dokuwiki SSO plugin: '.$msg); 151 } 152 153 /** 154 * Go to URL 155 */ 156 #[NoReturn] private function _goto(string $url, string $target = ''): void { 157 $url = $url ? str_replace('{target}', $target, $url) : $target; 158 159 $this->_log('redirecting user to '.$url); 160 header('Location: '.$url); 161 exit; 162 } 163 164 165 /** 166 * Check authentication 167 * 168 * @param string $user 169 * @param string $pass 170 * @param bool $sticky 171 */ 172 public function trustExternal($user, $pass, $sticky = false) { 173 $do = array_key_exists('do', $_REQUEST) ? $_REQUEST['do'] : null; 174 $autologin = $this->_getConf('autologin'); 175 $has_attributes = $this->_hasAttributes(); 176 $has_session = $this->_hasSession(); 177 178 $state = ['autologin' => $autologin, 'has_attributes' => $has_attributes, 'has_session' => $has_session]; 179 $state = preg_replace('`(\n|\s+)`', ' ', print_r($state, true)); 180 181 if($do === 'login' && !$has_attributes) 182 $this->_login(); 183 184 if($do === 'logout' && $has_attributes) 185 $this->_logout(); 186 187 if($do === 'login' || ($autologin && $has_attributes && !$has_session)) { 188 $attrs = $this->_getAttributes(); 189 $data = $this->getUserData($user); 190 $this->_setSession($attrs['id'], $data['grps'], $attrs['email'], $attrs['fullname']); 191 $this->_log('authenticated user (state='.$state.')'); 192 return; 193 } 194 195 if($do === 'logout' || ($autologin && !$has_attributes && $has_session)) { 196 $this->_dropSession(); 197 $this->_log('logged user out (state='.$state.')'); 198 return; 199 } 200 201 // Check user match is SSO and local session 202 if($autologin && $has_session && $has_attributes) { 203 if($_SESSION[DOKU_COOKIE]['auth']['user'] !== $this->_getAttributes(false, 'id')) { 204 $this->_log('SSO user doesn\'t match local user, logging out (state='.$state.')'); 205 $this->_logout(); 206 } 207 } 208 209 // Refresh from cookie if any 210 auth_login(null, null); 211 } 212 213 /** 214 * Check if local session exists 215 */ 216 private function _hasSession(): bool { 217 return array_key_exists(DOKU_COOKIE, $_SESSION) && array_key_exists('auth', $_SESSION[DOKU_COOKIE]) && $_SESSION[DOKU_COOKIE]['auth']['user']; 218 } 219 220 /** 221 * Create user session 222 */ 223 private function _setSession(string $user, array $grps = null, string $mail = null, string $name = null): void { 224 global $USERINFO; 225 global $INPUT; 226 227 $USERINFO['name'] = $name ?: $user; 228 $USERINFO['mail'] = $mail ?: (mail_isvalid($user) ? $user : null); 229 $USERINFO['grps'] = array_filter((array)$grps); 230 231 $INPUT->server->set('REMOTE_USER', $user); 232 233 $secret = auth_cookiesalt(true, true); 234 $pass = hash_hmac('sha1', $user, $secret); 235 auth_setCookie($user, auth_encrypt($pass, $secret), false); 236 237 $dummy = []; 238 trigger_event('AUTH_EXTERNAL', $dummy); 239 } 240 241 /** 242 * Remove session data 243 */ 244 private function _dropSession(): void { 245 auth_logoff(); 246 247 $dummy = []; 248 trigger_event('AUTH_EXTERNAL', $dummy); 249 } 250 251 /** 252 * Redirect for login 253 */ 254 #[NoReturn] public function _login(): void { 255 $this->_dropSession(); 256 $this->_goto($this->_getConf('login_url'), wl(getId())); 257 } 258 259 /** 260 * Redirect for logout 261 */ 262 #[NoReturn] public function _logout(): void { 263 $this->_dropSession(); 264 $this->_goto($this->_getConf('logout_url'), $this->_getConf('home_url')); 265 } 266 267 /** 268 * Check password (not used but required by inheritance) 269 * 270 * @param string $user 271 * @param string $pass 272 */ 273 public function checkPass($user, $pass): bool { 274 if($_SESSION[DOKU_COOKIE]['auth']['user'] !== $user) 275 return false; 276 277 $attrs = $this->_getAttributes(); 278 if($user !== $attrs['id']) 279 return false; 280 281 $secret = auth_cookiesalt(true, true); 282 if($pass !== hash_hmac('sha1', $user, $secret)) 283 return false; 284 285 return true; 286 } 287 288 /** 289 * Get user info 290 * 291 * @param string $user 292 * @param bool $requireGroups 293 */ 294 public function getUserData($user, $requireGroups = true): array { 295 if(is_null($this->_users)) { 296 $this->_users = []; 297 if(@file_exists(DOKU_CONF.'users.auth.php')) { 298 foreach(file(DOKU_CONF.'users.auth.php') as $line) { 299 $line = trim(preg_replace('/#.*$/', '', $line)); //ignore comments 300 if(!$line) continue; 301 $row = explode(':', $line, 5); 302 $this->_users[$row[0]] = [ 303 'pass' => $row[1], 304 'name' => urldecode($row[2]), 305 'mail' => $row[3], 306 'grps' => explode(',', $row[4]) 307 ]; 308 } 309 } 310 } 311 312 // Any user virtualy exists 313 $data = ['name' => $user, 'mail' => $user, 'grps' => []]; 314 315 global $INPUT; 316 if($user === $INPUT->server->str('REMOTE_USER')) { 317 $attrs = $this->_getAttributes(); 318 $data = ['name' => $attrs['fullname'], 'mail' => $attrs['email'], 'grps' => []]; 319 } 320 321 $grps = array_key_exists($user, $this->_users) ? $this->_users[$user]['grps'] : []; 322 $data['grps'] = array_unique(array_merge($grps, $data['grps'], ['session'])); 323 324 return $requireGroups ? $data : array_diff_key($data, ['grps' => null]); 325 } 326} 327