1<?php 2/** 3 * SAML authentication plugin 4 * 5 * @author Andreas Gohr <gohr@cosmocode.de> 6 * @author Sam Yelman <sam.yelman@temple.edu> 7 */ 8class auth_plugin_saml extends auth_plugin_authplain 9{ 10 /** @var OneLogin_Saml2_Auth the SAML authentication library */ 11 protected $saml; 12 13 /** @inheritdoc */ 14 public function __construct() 15 { 16 parent::__construct(); 17 18 $this->cando['external'] = true; 19 $this->cando['logoff'] = true; 20 /* We only want auth_plain for e-mail tracking and group storage */ 21 $this->cando['addUser'] = false; 22 $this->cando['modLogin'] = false; 23 $this->cando['modPass'] = false; 24 $this->cando['modName'] = false; 25 $this->cando['modMail'] = false; 26 $this->cando['modGroups'] = false; 27 28 /** @var helper_plugin_saml $hlp */ 29 $hlp = plugin_load('helper', 'saml'); 30 $this->saml = $hlp->getSamlLib(); 31 } 32 33 /** 34 * Checks the session to see if the user is already logged in 35 * 36 * If not logged in, redirects to SAML provider 37 */ 38 public function trustExternal($user, $pass, $sticky = false) 39 { 40 global $USERINFO; 41 global $ID; 42 global $ACT; 43 44 $autoLogin = false; 45 46 if (empty($ID)) $ID = getID(); 47 48 // trust session info, no need to recheck 49 if (isset($_SESSION[DOKU_COOKIE]['auth']) && 50 $_SESSION[DOKU_COOKIE]['auth']['buid'] == auth_browseruid() && 51 isset($_SESSION[DOKU_COOKIE]['auth']['user']) 52 ) { 53 54 $_SERVER['REMOTE_USER'] = $_SESSION[DOKU_COOKIE]['auth']['user']; 55 $USERINFO = $_SESSION[DOKU_COOKIE]['auth']['info']; 56 57 return true; 58 } else { 59 $autoLoginConf = $this->getConf("auto_login"); 60 $autoLogin = ($autoLoginConf == "never") ? false : ( 61 ($autoLoginConf == "after login" && get_doku_pref('saml_autologin', 0)) || 62 ($autoLoginConf == "always")); 63 } 64 65 if (!isset($_POST['SAMLResponse']) && ($ACT == 'login' || $autoLogin)) { 66 // Initiate SAML auth request 67 $url = $this->saml->login( 68 null, // returnTo: is configured in our settings 69 [], // parameter: we do not send any additional paramters to ndreas 70 false, // forceAuthn: would skip any available SSO data, not what we want 71 false, // isPassive: would avoid all user interaction, not what we want 72 true, // stay: do not redirect, we do that ourselves 73 false // setNamedIdPolicy: we need to disable this or SAML complains about our request 74 ); 75 $_SESSION['saml_redirect'] = wl($ID, '', true, '&'); // remember current page 76 send_redirect($url); 77 } elseif (isset($_POST['SAMLResponse'])) { 78 // consume SAML response 79 try { 80 $this->saml->processResponse(); 81 if ($this->saml->isAuthenticated()) { 82 // Always read the userid from the saml response 83 $USERINFO = $this->getUserDataFromResponse(); 84 $_SERVER['REMOTE_USER'] = $USERINFO['user']; 85 86 if ($this->getConf('autoprovisioning')) { 87 // In case of auto-provisionning we override the local DB info with those retrieve during the SAML negociation 88 if ( $this->getUserData($USERINFO['user']) === false ) { 89 $this->triggerUserMod('create', array( 90 $USERINFO['user'], 91 "\0\0nil\0\0", 92 $USERINFO['name'], 93 $USERINFO['mail'], 94 $USERINFO['grps'] 95 )); 96 } else { 97 $this->triggerUserMod('modify', array( 98 $USERINFO['user'], 99 $USERINFO 100 )); 101 } 102 } else { 103 // In case the autoprovisionning is disabled we rely on the local DB for the info such as the group and the fullname. 104 // It also means that the user should exists already in the DB 105 $dbUserInfo = $this->getUserData($USERINFO['user']); 106 if($dbUserInfo === false) throw new \Exception('This user is not in the local user database and may not login'); 107 $USERINFO['name'] = $dbUserInfo["name"]; 108 $USERINFO['mail'] = $dbUserInfo["mail"]; 109 $USERINFO['grps'] = $dbUserInfo["grps"]; 110 } 111 112 // Store that in the cookie 113 $_SESSION[DOKU_COOKIE]['auth']['user'] = $_SERVER['REMOTE_USER']; 114 $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO; 115 $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid(); // cache login 116 117 // successful login 118 if (isset($_SESSION['saml_redirect'])) { 119 $go = $_SESSION['saml_redirect']; 120 unset($_SESSION['saml_redirect']); 121 } else { 122 $go = wl($ID, '', true, '&'); 123 } 124 set_doku_pref('saml_autologin', 1); 125 send_redirect($go); // decouple the history from POST 126 return true; 127 } else { 128 $this->logOff(); 129 130 msg('SAML: '.hsc($this->saml->getLastErrorReason()), -1); 131 return false; 132 } 133 } catch (Exception $e) { 134 $this->logOff(); 135 msg('Invalid SAML response: ' . hsc($e->getMessage()), -1); 136 return false; 137 } 138 } 139 // no login happened 140 return false; 141 } 142 143 144 /** @inheritdoc */ 145 public function logOff() 146 { 147 global $ID; 148 set_doku_pref('saml_autologin', 0); 149 150 $hlp = plugin_load('helper', 'saml'); 151 $saml = $hlp->getSamlLib(); 152 153 /* By default, try to return to user to the page they were just viewing */ 154 $redirTo = wl($ID, '', true, '&'); 155 156 /* Proccess an SLO request or response */ 157 if(isset($_GET["SAMLResponse"]) || isset($_GET["SAMLRequest"])) { 158 $saml->processSLO(); 159 $errors = $saml->getErrors(); 160 161 if (!empty($errors)) { 162 msg('SAML SLO: '. implode(', ', $errors) . '; ' . $saml->getLastErrorReason(), -1); 163 } 164 165 /* If a RelayState is defined in the Request, this is where we want to redirect to afterwards */ 166 if(isset($_GET["RelayState"])) $redirTo = $_GET["RelayState"]; 167 168 /* If user initiates logout from the wiki itself */ 169 } else if($this->getConf('use_slo')) { 170 $saml->logout($redirTo); 171 } 172 173 /* Manually redirect user if we ever get here */ 174 send_redirect($redirTo); 175 } 176 177 /** @inheritdoc */ 178 public function cleanUser($user) 179 { 180 // strip disallowed characters 181 $user = strtr( 182 $user, array( 183 ',' => '', 184 '/' => '', 185 '#' => '', 186 ';' => '', 187 ':' => '' 188 ) 189 ); 190 if ($this->getConf('lowercase')) { 191 return utf8_strtolower($user); 192 } else { 193 return $user; 194 } 195 } 196 197 /** @inheritdoc */ 198 public function cleanGroup($group) 199 { 200 return $this->cleanUser($group); 201 } 202 203 204 /** 205 * Build user data from the response 206 * 207 * @return array the user data 208 * @throws Exception when attributes are missing 209 */ 210 protected function getUserDataFromResponse() 211 { 212 global $conf; 213 214 // which attributes should be in the response? 215 $attributes = [ 216 'user' => $this->getConf('userid_attr_name') 217 ]; 218 if ($this->getConf('autoprovisioning')) { 219 $attributes['name'] = $this->getConf('fullname_attr_name'); 220 if (empty($attributes['name'])) $attributes['name'] = $attributes['user']; // fall back to login 221 $attributes['mail'] = $this->getConf('email_attr_name'); 222 $attributes['grps'] = $this->getConf('groups_attr_name'); 223 if (empty($attributes['grps'])) unset($attributes['grps']); // groups are optional 224 } 225 226 // get attributes from response 227 $userdata = ['user' => '', 'mail' => '', 'name' => '', 'grps' => []]; 228 foreach ($attributes as $key => $attr) { 229 $data = $this->saml->getAttribute($attr); 230 if ($data === null) throw new \Exception('SAML Response is missing attribute ' . $attr); 231 $userdata[$key] = $data; 232 } 233 234 // clean up data 235 $userdata['user'] = $this->cleanUser($userdata['user'][0]); 236 $userdata['name'] = $userdata['name'][0]; 237 $userdata['mail'] = $userdata['mail'][0]; 238 $userdata['grps'] = (array)$userdata['grps']; 239 $userdata['grps'][] = $conf['defaultgroup']; 240 $userdata['grps'] = array_map([$this, 'cleanGroup'], $userdata['grps']); 241 242 return $userdata; 243 } 244} 245