cilogon /
service-lib
| 1 | <?php |
||
| 2 | |||
| 3 | namespace CILogon\Service; |
||
| 4 | |||
| 5 | use CILogon\Service\Util; |
||
| 6 | use CILogon\Service\Content; |
||
| 7 | |||
| 8 | /** |
||
| 9 | * PortalCookie |
||
| 10 | * |
||
| 11 | * This class is used by the 'CILogon Delegate Service' |
||
| 12 | * and the CILogon OIDC 'authorize' endpoint to keep track of |
||
| 13 | * user-selected attributes such as lifetime (in hours) of the |
||
| 14 | * delegated certificate and if the user clicked the 'Always Allow' |
||
| 15 | * button to remember the allowed delegation upon future accesses. |
||
| 16 | * The information related to certificate 'lifetime' and 'remember' the |
||
| 17 | * delegation settings is stored in a single cookie. Since the data |
||
| 18 | * is actually a two dimensional array (first element is the name of |
||
| 19 | * the portal, the second element is an array of the various |
||
| 20 | * attributes), the stored cookie is actually a base64-encoded |
||
| 21 | * serialization of the 2D array. This class provides methods to |
||
| 22 | * read/write the cookie, and to get/set the values for a given portal. |
||
| 23 | * |
||
| 24 | * Example usage: |
||
| 25 | * require_once 'PortalCookie.php'; |
||
| 26 | * $pc = new PortalCookie(); // Automatically reads the cookie |
||
| 27 | * // Assume the callbackuri or redirect_uri for the portal has |
||
| 28 | * // been set in the PHP session. |
||
| 29 | * $lifetime = $pc->get('lifetime'); |
||
| 30 | * if ($lifetime < 1) { |
||
| 31 | * $lifetime = 1; |
||
| 32 | * } elseif ($lifetime > 240) { |
||
| 33 | * $lifetime = 240; |
||
| 34 | * } |
||
| 35 | * $pc->set('remember',1); |
||
| 36 | * $pc->write(); // Must be done before any HTML output |
||
| 37 | */ |
||
| 38 | class PortalCookie |
||
| 39 | { |
||
| 40 | /** |
||
| 41 | * @var string COOKIENAME The token name is const to be accessible from |
||
| 42 | * removeTheCookie. |
||
| 43 | */ |
||
| 44 | public const COOKIENAME = "portalparams"; |
||
| 45 | |||
| 46 | /** |
||
| 47 | * @var array $portalarray An array of arrays. First index is portal name. |
||
| 48 | */ |
||
| 49 | public $portalarray = array(); |
||
| 50 | |||
| 51 | /** |
||
| 52 | * __construct |
||
| 53 | * |
||
| 54 | * Default constructor. This reads the current portal cookie into |
||
| 55 | * the class $portalarray arary. |
||
| 56 | */ |
||
| 57 | public function __construct() |
||
| 58 | { |
||
| 59 | $this->read(); |
||
| 60 | } |
||
| 61 | |||
| 62 | /** |
||
| 63 | * read |
||
| 64 | * |
||
| 65 | * This method reads the portal cookie, decodes the base64 string, |
||
| 66 | * decrypts the AES-128-CBC string, and unserializes the 2D array. |
||
| 67 | * This is stored in the class $portalarray array. |
||
| 68 | */ |
||
| 69 | public function read() |
||
| 70 | { |
||
| 71 | if (isset($_COOKIE[static::COOKIENAME])) { |
||
| 72 | $cookievar = $_COOKIE[static::COOKIENAME]; |
||
| 73 | $serial = $cookievar; |
||
| 74 | |||
| 75 | // Attempt to un-base64 and decrypt portal array from cookie |
||
| 76 | if (defined('OPENSSL_KEY') && (!empty(OPENSSL_KEY))) { |
||
|
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||
| 77 | $b64 = base64_decode($cookievar); |
||
| 78 | if ($b64 !== false) { |
||
| 79 | $iv = substr($b64, 0, 16); // IV prepended to encrypted data |
||
| 80 | $b64a = substr($b64, 16); // IV is 16 bytes, rest is data |
||
| 81 | if ((strlen($iv) > 0) && (strlen($b64a) > 0)) { |
||
| 82 | $serial = openssl_decrypt( |
||
| 83 | $b64a, |
||
| 84 | 'AES-128-CBC', |
||
| 85 | OPENSSL_KEY, |
||
| 86 | OPENSSL_RAW_DATA, |
||
| 87 | $iv |
||
| 88 | ); |
||
| 89 | } |
||
| 90 | } |
||
| 91 | } |
||
| 92 | |||
| 93 | // Unserialize the cookie data back into the portalarray |
||
| 94 | if (strlen($serial) > 0) { |
||
| 95 | $unserial = unserialize($serial); |
||
| 96 | if ($unserial !== false) { |
||
| 97 | $this->portalarray = $unserial; |
||
| 98 | } |
||
| 99 | } |
||
| 100 | } |
||
| 101 | } |
||
| 102 | |||
| 103 | /** |
||
| 104 | * write |
||
| 105 | * |
||
| 106 | * This method writes the class $portalarray to a cookie. In |
||
| 107 | * order to store the 2D array as a cookie, the array is first |
||
| 108 | * serialized, then encrypted with AES-128-CBC, and then base64- |
||
| 109 | * encoded. |
||
| 110 | */ |
||
| 111 | public function write() |
||
| 112 | { |
||
| 113 | if (!empty($this->portalarray)) { |
||
| 114 | $this->set('ut', time()); // Save update time |
||
| 115 | $serial = serialize($this->portalarray); |
||
| 116 | // Special check: If the serialization of the cookie is |
||
| 117 | // more than 2500 bytes, the resulting base64-encoded string |
||
| 118 | // may be too big (>4K). So scan through all portal entries |
||
| 119 | // and delete the oldest one until the size is small enough. |
||
| 120 | while (strlen($serial) > 2500) { |
||
| 121 | $smallvalue = 5000000000; // Unix time = Jun 11, 2128 |
||
| 122 | $smallportal = ''; |
||
| 123 | foreach ($this->portalarray as $k => $v) { |
||
| 124 | if (isset($v['ut'])) { |
||
| 125 | if ($v['ut'] < $smallvalue) { |
||
| 126 | $smallvalue = $v['ut']; |
||
| 127 | $smallportal = $k; |
||
| 128 | } |
||
| 129 | } else { // 'ut' not set, delete it |
||
| 130 | $smallportal = $k; |
||
| 131 | break; |
||
| 132 | } |
||
| 133 | } |
||
| 134 | if (strlen($smallportal) > 0) { |
||
| 135 | unset($this->portalarray[$smallportal]); |
||
| 136 | } else { |
||
| 137 | break; // Should never get here, but just in case |
||
| 138 | } |
||
| 139 | $serial = serialize($this->portalarray); |
||
| 140 | } |
||
| 141 | $cookievar = $serial; |
||
| 142 | |||
| 143 | // Attempt to encrypt and base64 the serialized portal array |
||
| 144 | if (defined('OPENSSL_KEY') && (!empty(OPENSSL_KEY))) { |
||
|
0 ignored issues
–
show
|
|||
| 145 | $iv = openssl_random_pseudo_bytes(16); // IV is 16 bytes |
||
| 146 | if (strlen($iv) > 0) { |
||
| 147 | $data = openssl_encrypt( |
||
| 148 | $cookievar, |
||
| 149 | 'AES-128-CBC', |
||
| 150 | OPENSSL_KEY, |
||
| 151 | OPENSSL_RAW_DATA, |
||
| 152 | $iv |
||
| 153 | ); |
||
| 154 | if (strlen($data) > 0) { |
||
| 155 | $b64 = base64_encode($iv . $data); // Prepend IV to data |
||
| 156 | if ($b64 !== false) { |
||
| 157 | $cookievar = $b64; |
||
| 158 | } |
||
| 159 | } |
||
| 160 | } |
||
| 161 | } |
||
| 162 | Util::setCookieVar(static::COOKIENAME, $cookievar); |
||
| 163 | } |
||
| 164 | } |
||
| 165 | |||
| 166 | /** |
||
| 167 | * getPortalName |
||
| 168 | * |
||
| 169 | * This method looks in the PHP session for one of 'callbackuri' |
||
| 170 | * (in the OAuth 1.0a 'delegate' case) or various $clientparams |
||
| 171 | * (in the OIDC 'authorize' case).This is used as the key for the |
||
| 172 | * $portalarray. If neither of these session variables is set, |
||
| 173 | * return empty string. |
||
| 174 | * |
||
| 175 | * @return string The name of the portal, which is either the |
||
| 176 | * OAuth 1.0a 'callbackuri' or the OIDC client info. |
||
| 177 | */ |
||
| 178 | public function getPortalName() |
||
| 179 | { |
||
| 180 | // Check the OAuth 1.0a 'delegate' 'callbackuri' |
||
| 181 | $retval = Util::getSessionVar('callbackuri'); |
||
| 182 | if (strlen($retval) == 0) { |
||
| 183 | // Next, check the OAuth 2.0 'authorize' $clientparams[] |
||
| 184 | $clientparams = json_decode( |
||
| 185 | Util::getSessionVar('clientparams'), |
||
| 186 | true |
||
| 187 | ); |
||
| 188 | if ( |
||
| 189 | (isset($clientparams['client_id'])) && |
||
| 190 | (isset($clientparams['redirect_uri'])) && |
||
| 191 | (isset($clientparams['scope'])) |
||
| 192 | ) { |
||
| 193 | // Use the first element of the idphint list as the selected_idp. |
||
| 194 | $selected_idp = ''; |
||
| 195 | $idphintlist = Content::getIdphintList(); |
||
| 196 | if (!empty($idphintlist)) { |
||
| 197 | $selected_idp = $idphintlist[0]; |
||
| 198 | } |
||
| 199 | $retval = $clientparams['client_id'] . ';' . |
||
| 200 | $clientparams['redirect_uri'] . ';' . |
||
| 201 | $clientparams['scope'] . |
||
| 202 | (empty($selected_idp) ? '' : ';' . $selected_idp); |
||
| 203 | } |
||
| 204 | } |
||
| 205 | return $retval; |
||
| 206 | } |
||
| 207 | |||
| 208 | /** |
||
| 209 | * removeTheCookie |
||
| 210 | * |
||
| 211 | * This method unsets the portal cookie in the user's browser. |
||
| 212 | * This should be called before any HTML is output. |
||
| 213 | */ |
||
| 214 | public static function removeTheCookie() |
||
| 215 | { |
||
| 216 | Util::unsetCookieVar(static::COOKIENAME); |
||
| 217 | } |
||
| 218 | |||
| 219 | /** |
||
| 220 | * get |
||
| 221 | * |
||
| 222 | * This method is a generalized getter to fetch the value of a |
||
| 223 | * parameter for a given portal. In other words, this method |
||
| 224 | * returns $this->portalarray[$param], where $param is something |
||
| 225 | * like 'lifetime' or 'remember'. If the portal name is not set, |
||
| 226 | * or the requested parameter is missing from the cookie, return |
||
| 227 | * empty string. |
||
| 228 | * |
||
| 229 | * @param string $param The attribute of the portal to get. Should be |
||
| 230 | * something like 'lifetime' or 'remember'. |
||
| 231 | * @return string The value of the $param for the portal. |
||
| 232 | */ |
||
| 233 | public function get($param) |
||
| 234 | { |
||
| 235 | $retval = ''; |
||
| 236 | $name = $this->getPortalName(); |
||
| 237 | if (strlen($name) > 0) { |
||
| 238 | if ( |
||
| 239 | (isset($this->portalarray[$name])) && |
||
| 240 | (isset($this->portalarray[$name][$param])) |
||
| 241 | ) { |
||
| 242 | $retval = $this->portalarray[$name][$param]; |
||
| 243 | } elseif ($param == 'providerId') { |
||
| 244 | // CIL-719 If there is no portal cookie set for this |
||
| 245 | // particular 'portal name', then attempt to read the |
||
| 246 | // 'providerId' value from the most recent portal cookie. |
||
| 247 | $pa = $this->portalarray; // Make a copy of the portalarary |
||
| 248 | // Ascending sort the array by 'ut' |
||
| 249 | uasort($pa, function ($a, $b) { |
||
| 250 | return ($a['ut'] < $b['ut']) ? -1 : (($a['ut'] > $b['ut']) ? 1 : 0); |
||
| 251 | }); |
||
| 252 | // Get the last (most recent) element of the array |
||
| 253 | $name = @array_key_last($pa); |
||
| 254 | if ( |
||
| 255 | (strlen($name) > 0) && |
||
| 256 | (isset($pa[$name])) && |
||
| 257 | (isset($pa[$name][$param])) |
||
| 258 | ) { |
||
| 259 | $retval = $pa[$name][$param]; |
||
| 260 | } |
||
| 261 | } |
||
| 262 | } |
||
| 263 | return $retval; |
||
| 264 | } |
||
| 265 | |||
| 266 | /** |
||
| 267 | * set |
||
| 268 | * |
||
| 269 | * This method sets a portal's parameter to a given value. Note |
||
| 270 | * that $value should be an integer or character value. |
||
| 271 | * |
||
| 272 | * @param string $param The parameter of the portal to set. Should be |
||
| 273 | * something like 'lifetime' or 'remember'. |
||
| 274 | * @param string $value The value to set for the parameter. |
||
| 275 | */ |
||
| 276 | public function set($param, $value) |
||
| 277 | { |
||
| 278 | $name = $this->getPortalName(); |
||
| 279 | if (strlen($name) > 0) { |
||
| 280 | $this->portalarray[$name][$param] = $value; |
||
| 281 | } |
||
| 282 | } |
||
| 283 | |||
| 284 | /** |
||
| 285 | * __toString |
||
| 286 | * |
||
| 287 | * This function returns a string representation of the object. |
||
| 288 | * The format is 'portal=...,lifetime=...,remember=...'. Multiple |
||
| 289 | * portals are separated by a newline character. |
||
| 290 | * |
||
| 291 | * @return string A 'pretty print' representation of the class |
||
| 292 | * portalarray. |
||
| 293 | */ |
||
| 294 | public function __toString() |
||
| 295 | { |
||
| 296 | $retval = ''; |
||
| 297 | $first = true; |
||
| 298 | foreach ($this->portalarray as $key => $value) { |
||
| 299 | if (!$first) { |
||
| 300 | $retval .= "\n"; |
||
| 301 | } |
||
| 302 | $first = false; |
||
| 303 | $retval .= 'portal=' . $key; |
||
| 304 | ksort($value); |
||
| 305 | foreach ($value as $key2 => $value2) { |
||
| 306 | $retval .= ", $key2=$value2"; |
||
| 307 | } |
||
| 308 | } |
||
| 309 | return $retval; |
||
| 310 | } |
||
| 311 | } |
||
| 312 |