Rev 1300 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 1300 | Rev 1301 | ||
---|---|---|---|
Line 23... | Line 23... | ||
23 | \defined('INSIDE_OIDPLUS') or die; |
23 | \defined('INSIDE_OIDPLUS') or die; |
24 | // phpcs:enable PSR1.Files.SideEffects |
24 | // phpcs:enable PSR1.Files.SideEffects |
25 | 25 | ||
26 | class OIDplusAuthContentStoreSession extends OIDplusAuthContentStore { |
26 | class OIDplusAuthContentStoreSession extends OIDplusAuthContentStore { |
27 | 27 | ||
28 | /** |
- | |
29 | * @return OIDplusSessionHandler |
- | |
30 | */ |
- | |
31 | protected static function getSessionHandler(): OIDplusSessionHandler { |
- | |
32 | static $sesHandler = null; |
- | |
33 | if (is_null($sesHandler)) { |
- | |
34 | $sesHandler = new OIDplusSessionHandler(); |
- | |
35 | } |
- | |
36 | return $sesHandler; |
- | |
37 | } |
- | |
38 | - | ||
39 | // Override abstract functions |
28 | // Override abstract functions |
40 | # TODO: shouldn't we just include OIDplusSessionHandler in this class? |
- | |
41 | 29 | ||
42 | /** |
30 | /** |
43 | * @param string $name |
31 | * @param string $name |
44 | * @param mixed|null $default |
32 | * @param mixed|null $default |
45 | * @return mixed|null |
33 | * @return mixed|null |
46 | * @throws OIDplusException |
34 | * @throws OIDplusException |
47 | */ |
35 | */ |
48 | public function getValue(string $name, $default = NULL) { |
36 | public function getValue(string $name, $default = NULL) { |
49 | try { |
37 | try { |
- | 38 | if (isset($this->cacheSetValues[$name])) return self::decrypt($this->cacheSetValues[$name], $this->secret); |
|
- | 39 | ||
- | 40 | if (!$this->isActive()) return $default; // GDPR: Only start a session when we really need one |
|
- | 41 | $this->sessionSafeStart(); |
|
- | 42 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
|
- | 43 | ||
- | 44 | if (!isset($_SESSION[$name])) return $default; |
|
50 | return self::getSessionHandler()->getValue($name, $default); |
45 | return self::decrypt($_SESSION[$name], $this->secret); |
51 | } catch (\Exception $e) { |
46 | } catch (\Exception $e) { |
52 | self::getSessionHandler()->destroySession(); |
47 | $this->destroySession(); |
53 | // TODO: For some reason If destroySession() is called, we won't get this Exception?! |
48 | // TODO: For some reason If destroySession() is called, we won't get this Exception?! |
54 | throw new OIDplusException(_L('Internal error with session. Please reload the page and log-in again. %1', $e->getMessage())); |
49 | throw new OIDplusException(_L('Internal error with session. Please reload the page and log-in again. %1', $e->getMessage())); |
55 | } |
50 | } |
56 | } |
51 | } |
57 | 52 | ||
Line 60... | Line 55... | ||
60 | * @param mixed $value |
55 | * @param mixed $value |
61 | * @return void |
56 | * @return void |
62 | * @throws OIDplusException |
57 | * @throws OIDplusException |
63 | */ |
58 | */ |
64 | public function setValue(string $name, $value) { |
59 | public function setValue(string $name, $value) { |
- | 60 | $enc_data = self::encrypt($value, $this->secret); |
|
- | 61 | ||
65 | self::getSessionHandler()->setValue($name, $value); |
62 | $this->cacheSetValues[$name] = $enc_data; |
- | 63 | ||
- | 64 | $this->sessionSafeStart(); |
|
- | 65 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
|
- | 66 | ||
- | 67 | $_SESSION[$name] = $enc_data; |
|
66 | } |
68 | } |
67 | 69 | ||
68 | /** |
70 | /** |
69 | * @param string $name |
71 | * @param string $name |
70 | * @return bool |
72 | * @return bool |
71 | * @throws OIDplusException |
73 | * @throws OIDplusException |
72 | */ |
74 | */ |
73 | public function exists(string $name): bool { |
75 | public function exists(string $name): bool { |
- | 76 | if (isset($this->cacheSetValues[$name])) return true; |
|
- | 77 | ||
- | 78 | if (!$this->isActive()) return false; // GDPR: Only start a session when we really need one |
|
- | 79 | $this->sessionSafeStart(); |
|
- | 80 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
|
- | 81 | ||
74 | return self::getSessionHandler()->exists($name); |
82 | return isset($_SESSION[$name]); |
75 | } |
83 | } |
76 | 84 | ||
77 | /** |
85 | /** |
78 | * @param string $name |
86 | * @param string $name |
79 | * @return void |
87 | * @return void |
80 | * @throws OIDplusException |
88 | * @throws OIDplusException |
81 | */ |
89 | */ |
82 | public function delete(string $name) { |
90 | public function delete(string $name) { |
- | 91 | if (isset($this->cacheSetValues[$name])) unset($this->cacheSetValues[$name]); |
|
- | 92 | ||
- | 93 | if (!$this->isActive()) return; // GDPR: Only start a session when we really need one |
|
83 | self::getSessionHandler()->delete($name); |
94 | $this->sessionSafeStart(); |
- | 95 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
|
- | 96 | ||
- | 97 | unset($_SESSION[$name]); |
|
84 | } |
98 | } |
85 | 99 | ||
86 | /** |
100 | /** |
87 | * @return void |
101 | * @return void |
88 | * @throws OIDplusException |
102 | * @throws OIDplusException |
89 | */ |
103 | */ |
90 | public function destroySession() { |
104 | public function destroySession() { |
- | 105 | if (!$this->isActive()) return; |
|
- | 106 | ||
- | 107 | $this->sessionSafeStart(); |
|
- | 108 | OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime); |
|
- | 109 | ||
- | 110 | $_SESSION = array(); |
|
91 | self::getSessionHandler()->destroySession(); |
111 | session_destroy(); |
- | 112 | session_write_close(); |
|
- | 113 | OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy |
|
92 | } |
114 | } |
93 | 115 | ||
94 | /** |
116 | /** |
95 | * @return OIDplusAuthContentStoreSession|null |
117 | * @return OIDplusAuthContentStoreSession|null |
- | 118 | * @throws OIDplusException |
|
96 | */ |
119 | */ |
97 | public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
120 | public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ { |
98 | static $contentProvider = null; |
121 | static $contentProvider = null; |
99 | 122 | ||
100 | $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT))); |
123 | $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT))); |
Line 102... | Line 125... | ||
102 | // For REST, we must only allow JWT from Bearer and nothing else! So disable cookies if we are accessing the REST plugin |
125 | // For REST, we must only allow JWT from Bearer and nothing else! So disable cookies if we are accessing the REST plugin |
103 | return null; |
126 | return null; |
104 | } |
127 | } |
105 | 128 | ||
106 | if (!$contentProvider) { |
129 | if (!$contentProvider) { |
107 | if (self::getSessionHandler()->isActive()) { |
130 | if (self::isActive()) { |
108 | $contentProvider = new OIDplusAuthContentStoreSession(); |
131 | $contentProvider = new OIDplusAuthContentStoreSession(); |
109 | } |
132 | } |
110 | } |
133 | } |
111 | 134 | ||
112 | return $contentProvider; |
135 | return $contentProvider; |
Line 114... | Line 137... | ||
114 | 137 | ||
115 | /** |
138 | /** |
116 | * @param string $email |
139 | * @param string $email |
117 | * @param string $loginfo |
140 | * @param string $loginfo |
118 | * @return void |
141 | * @return void |
- | 142 | * @throws OIDplusException |
|
119 | */ |
143 | */ |
120 | public function raLoginEx(string $email, string &$loginfo) { |
144 | public function raLoginEx(string $email, string &$loginfo) { |
121 | $this->raLogin($email); |
145 | $this->raLogin($email); |
122 | if (is_null(self::getActiveProvider())) { |
146 | if (is_null(self::getActiveProvider())) { |
123 | $loginfo = 'into new PHP session'; |
147 | $loginfo = 'into new PHP session'; |
Line 127... | Line 151... | ||
127 | } |
151 | } |
128 | 152 | ||
129 | /** |
153 | /** |
130 | * @param string $loginfo |
154 | * @param string $loginfo |
131 | * @return void |
155 | * @return void |
- | 156 | * @throws OIDplusException |
|
132 | */ |
157 | */ |
133 | public function adminLoginEx(string &$loginfo) { |
158 | public function adminLoginEx(string &$loginfo) { |
134 | $this->adminLogin(); |
159 | $this->adminLogin(); |
135 | if (is_null(self::getActiveProvider())) { |
160 | if (is_null(self::getActiveProvider())) { |
136 | $loginfo = 'into new PHP session'; |
161 | $loginfo = 'into new PHP session'; |
Line 163... | Line 188... | ||
163 | */ |
188 | */ |
164 | public function activate() { |
189 | public function activate() { |
165 | # Sessions automatically activate during setValue() |
190 | # Sessions automatically activate during setValue() |
166 | } |
191 | } |
167 | 192 | ||
- | 193 | # ------------------------------------------------------------------------------------------------------------------ |
|
- | 194 | ||
- | 195 | /** |
|
- | 196 | * @var string|null |
|
- | 197 | */ |
|
- | 198 | private $secret = ''; |
|
- | 199 | ||
- | 200 | /** |
|
- | 201 | * @var int|null |
|
- | 202 | */ |
|
- | 203 | protected $sessionLifetime = 0; |
|
- | 204 | ||
- | 205 | /** |
|
- | 206 | * @throws OIDplusException |
|
- | 207 | */ |
|
- | 208 | public function __construct() { |
|
- | 209 | $this->sessionLifetime = OIDplus::baseConfig()->getValue('SESSION_LIFETIME', 30*60); |
|
- | 210 | $this->secret = OIDplus::authUtils()->makeSecret(['b118abc8-f4ec-11ed-86ca-3c4a92df8582']); |
|
- | 211 | ||
- | 212 | // **PREVENTING SESSION HIJACKING** |
|
- | 213 | // Prevents javascript XSS attacks aimed to steal the session ID |
|
- | 214 | @ini_set('session.cookie_httponly', '1'); |
|
- | 215 | ||
- | 216 | // **PREVENTING SESSION FIXATION** |
|
- | 217 | // Session ID cannot be passed through URLs |
|
- | 218 | @ini_set('session.use_only_cookies', '1'); |
|
- | 219 | ||
- | 220 | @ini_set('session.use_trans_sid', '0'); |
|
- | 221 | ||
- | 222 | // Uses a secure connection (HTTPS) if possible |
|
- | 223 | @ini_set('session.cookie_secure', OIDplus::isSslAvailable() ? '1' : '0'); |
|
- | 224 | ||
- | 225 | $path = OIDplus::webpath(null,OIDplus::PATH_RELATIVE); |
|
- | 226 | if (empty($path)) $path = '/'; |
|
- | 227 | @ini_set('session.cookie_path', $path); |
|
- | 228 | ||
- | 229 | @ini_set('session.cookie_samesite', OIDplus::baseConfig()->getValue('COOKIE_SAMESITE_POLICY', 'Strict')); |
|
- | 230 | ||
- | 231 | @ini_set('session.use_strict_mode', '1'); |
|
- | 232 | ||
- | 233 | @ini_set('session.gc_maxlifetime', $this->sessionLifetime); |
|
- | 234 | } |
|
- | 235 | ||
- | 236 | /** |
|
- | 237 | * @return void |
|
- | 238 | * @throws OIDplusException |
|
- | 239 | */ |
|
- | 240 | protected function sessionSafeStart() { |
|
- | 241 | if (!isset($_SESSION)) { |
|
- | 242 | // TODO: session_name() makes some problems. Leave it away for now. |
|
- | 243 | //session_name('OIDplus_SESHDLR'); |
|
- | 244 | if (!session_start()) { |
|
- | 245 | throw new OIDplusException(_L('Session could not be started')); |
|
- | 246 | } |
|
- | 247 | } |
|
- | 248 | ||
- | 249 | if (!isset($_SESSION['ip'])) { |
|
- | 250 | if (!isset($_SERVER['REMOTE_ADDR'])) return; |
|
- | 251 | ||
- | 252 | // Remember the IP address of the user |
|
- | 253 | $_SESSION['ip'] = $_SERVER['REMOTE_ADDR']; |
|
- | 254 | } else { |
|
- | 255 | if ($_SERVER['REMOTE_ADDR'] != $_SESSION['ip']) { |
|
- | 256 | // Was the session hijacked?! Get out of here! |
|
- | 257 | ||
- | 258 | // We don't use $this->destroySession(), because this calls sessionSafeStart() again |
|
- | 259 | $_SESSION = array(); |
|
- | 260 | session_destroy(); |
|
- | 261 | session_write_close(); |
|
- | 262 | OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy |
|
- | 263 | } |
|
- | 264 | } |
|
- | 265 | } |
|
- | 266 | ||
- | 267 | /** |
|
- | 268 | * @return void |
|
- | 269 | */ |
|
- | 270 | function __destruct() { |
|
- | 271 | session_write_close(); |
|
- | 272 | } |
|
- | 273 | ||
- | 274 | private $cacheSetValues = array(); // Important if you do a setValue() followed by an getValue() |
|
- | 275 | ||
- | 276 | /** |
|
- | 277 | * @return bool |
|
- | 278 | */ |
|
- | 279 | public static function isActive(): bool { |
|
- | 280 | return isset($_COOKIE[session_name()]); |
|
- | 281 | } |
|
- | 282 | ||
- | 283 | /** |
|
- | 284 | * @param string $data |
|
- | 285 | * @param string $key |
|
- | 286 | * @return string |
|
- | 287 | * @throws \Exception |
|
- | 288 | */ |
|
- | 289 | protected static function encrypt(string $data, string $key): string { |
|
- | 290 | if (function_exists('openssl_encrypt')) { |
|
- | 291 | $iv = random_bytes(16); // AES block size in CBC mode |
|
- | 292 | // Encryption |
|
- | 293 | $ciphertext = openssl_encrypt( |
|
- | 294 | $data, |
|
- | 295 | 'AES-256-CBC', |
|
- | 296 | hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true), |
|
- | 297 | OPENSSL_RAW_DATA, |
|
- | 298 | $iv |
|
- | 299 | ); |
|
- | 300 | // Authentication |
|
- | 301 | $hmac = sha3_512_hmac($iv . $ciphertext, $key, true); |
|
- | 302 | return $hmac . $iv . $ciphertext; |
|
- | 303 | } else { |
|
- | 304 | // When OpenSSL is not available, then we just do a HMAC |
|
- | 305 | $hmac = sha3_512_hmac($data, $key, true); |
|
- | 306 | return $hmac . $data; |
|
- | 307 | } |
|
- | 308 | } |
|
- | 309 | ||
- | 310 | /** |
|
- | 311 | * @param string $data |
|
- | 312 | * @param string $key |
|
- | 313 | * @return string |
|
- | 314 | * @throws OIDplusException |
|
- | 315 | */ |
|
- | 316 | protected static function decrypt(string $data, string $key): string { |
|
- | 317 | if (function_exists('openssl_decrypt')) { |
|
- | 318 | $hmac = mb_substr($data, 0, 64, '8bit'); |
|
- | 319 | $iv = mb_substr($data, 64, 16, '8bit'); |
|
- | 320 | $ciphertext = mb_substr($data, 80, null, '8bit'); |
|
- | 321 | // Authentication |
|
- | 322 | $hmacNew = sha3_512_hmac($iv . $ciphertext, $key, true); |
|
- | 323 | if (!hash_equals($hmac, $hmacNew)) { |
|
- | 324 | throw new OIDplusException(_L('Authentication failed')); |
|
- | 325 | } |
|
- | 326 | // Decryption |
|
- | 327 | $cleartext = openssl_decrypt( |
|
- | 328 | $ciphertext, |
|
- | 329 | 'AES-256-CBC', |
|
- | 330 | hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, true), |
|
- | 331 | OPENSSL_RAW_DATA, |
|
- | 332 | $iv |
|
- | 333 | ); |
|
- | 334 | if ($cleartext === false) { |
|
- | 335 | throw new OIDplusException(_L('Decryption failed')); |
|
- | 336 | } |
|
- | 337 | return $cleartext; |
|
- | 338 | } else { |
|
- | 339 | // When OpenSSL is not available, then we just do a HMAC |
|
- | 340 | $hmac = mb_substr($data, 0, 64, '8bit'); |
|
- | 341 | $cleartext = mb_substr($data, 64, null, '8bit'); |
|
- | 342 | $hmacNew = sha3_512_hmac($cleartext, $key, true); |
|
- | 343 | if (!hash_equals($hmac, $hmacNew)) { |
|
- | 344 | throw new OIDplusException(_L('Authentication failed')); |
|
- | 345 | } |
|
- | 346 | return $cleartext; |
|
- | 347 | } |
|
- | 348 | } |
|
- | 349 | ||
- | 350 | ||
168 | } |
351 | } |