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