Subversion Repositories oidplus

Rev

Rev 1300 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
566 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
566 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
566 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
566 daniel-mar 26
class OIDplusAuthContentStoreSession extends OIDplusAuthContentStore {
27
 
28
        // Override abstract functions
29
 
1116 daniel-mar 30
        /**
31
         * @param string $name
32
         * @param mixed|null $default
33
         * @return mixed|null
34
         * @throws OIDplusException
35
         */
36
        public function getValue(string $name, $default = NULL) {
716 daniel-mar 37
                try {
1301 daniel-mar 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;
45
                        return self::decrypt($_SESSION[$name], $this->secret);
1050 daniel-mar 46
                } catch (\Exception $e) {
1301 daniel-mar 47
                        $this->destroySession();
847 daniel-mar 48
                        // TODO: For some reason If destroySession() is called, we won't get this Exception?!
716 daniel-mar 49
                        throw new OIDplusException(_L('Internal error with session. Please reload the page and log-in again. %1', $e->getMessage()));
50
                }
566 daniel-mar 51
        }
52
 
1116 daniel-mar 53
        /**
54
         * @param string $name
55
         * @param mixed $value
56
         * @return void
57
         * @throws OIDplusException
58
         */
59
        public function setValue(string $name, $value) {
1301 daniel-mar 60
                $enc_data = self::encrypt($value, $this->secret);
61
 
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;
566 daniel-mar 68
        }
69
 
1116 daniel-mar 70
        /**
71
         * @param string $name
72
         * @return bool
73
         * @throws OIDplusException
74
         */
75
        public function exists(string $name): bool {
1301 daniel-mar 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
 
82
                return isset($_SESSION[$name]);
569 daniel-mar 83
        }
84
 
1116 daniel-mar 85
        /**
86
         * @param string $name
87
         * @return void
88
         * @throws OIDplusException
89
         */
90
        public function delete(string $name) {
1301 daniel-mar 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
94
                $this->sessionSafeStart();
95
                OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
96
 
97
                unset($_SESSION[$name]);
569 daniel-mar 98
        }
99
 
1116 daniel-mar 100
        /**
101
         * @return void
102
         * @throws OIDplusException
103
         */
585 daniel-mar 104
        public function destroySession() {
1301 daniel-mar 105
                if (!$this->isActive()) return;
106
 
107
                $this->sessionSafeStart();
108
                OIDplus::cookieUtils()->setcookie(session_name(),session_id(),time()+$this->sessionLifetime);
109
 
110
                $_SESSION = array();
111
                session_destroy();
112
                session_write_close();
113
                OIDplus::cookieUtils()->unsetcookie(session_name()); // remove cookie, so GDPR people are happy
566 daniel-mar 114
        }
115
 
1116 daniel-mar 116
        /**
117
         * @return OIDplusAuthContentStoreSession|null
1301 daniel-mar 118
         * @throws OIDplusException
1116 daniel-mar 119
         */
120
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
585 daniel-mar 121
                static $contentProvider = null;
122
 
1300 daniel-mar 123
                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
124
                if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core. (Maybe some kind of "stateless mode" that is enabled by 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
126
                        return null;
127
                }
128
 
585 daniel-mar 129
                if (!$contentProvider) {
1301 daniel-mar 130
                        if (self::isActive()) {
585 daniel-mar 131
                                $contentProvider = new OIDplusAuthContentStoreSession();
132
                        }
133
                }
134
 
135
                return $contentProvider;
136
        }
137
 
1116 daniel-mar 138
        /**
139
         * @param string $email
140
         * @param string $loginfo
141
         * @return void
1301 daniel-mar 142
         * @throws OIDplusException
1116 daniel-mar 143
         */
144
        public function raLoginEx(string $email, string &$loginfo) {
585 daniel-mar 145
                $this->raLogin($email);
146
                if (is_null(self::getActiveProvider())) {
147
                        $loginfo = 'into new PHP session';
148
                } else {
149
                        $loginfo = 'into existing PHP session';
150
                }
151
        }
152
 
1116 daniel-mar 153
        /**
154
         * @param string $loginfo
155
         * @return void
1301 daniel-mar 156
         * @throws OIDplusException
1116 daniel-mar 157
         */
158
        public function adminLoginEx(string &$loginfo) {
585 daniel-mar 159
                $this->adminLogin();
160
                if (is_null(self::getActiveProvider())) {
161
                        $loginfo = 'into new PHP session';
162
                } else {
163
                        $loginfo = 'into existing PHP session';
164
                }
165
        }
166
 
1116 daniel-mar 167
        /**
168
         * @param string $email
169
         * @param string $loginfo
170
         * @return void
171
         */
172
        public function raLogoutEx(string $email, string &$loginfo) {
585 daniel-mar 173
                $this->raLogout($email);
174
                $loginfo = 'from PHP session';
175
        }
176
 
1116 daniel-mar 177
        /**
178
         * @param string $loginfo
179
         * @return void
180
         */
181
        public function adminLogoutEx(string &$loginfo) {
585 daniel-mar 182
                $this->adminLogout();
183
                $loginfo = 'from PHP session';
184
        }
185
 
1116 daniel-mar 186
        /**
187
         * @return void
188
         */
585 daniel-mar 189
        public function activate() {
190
                # Sessions automatically activate during setValue()
191
        }
192
 
1301 daniel-mar 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
 
569 daniel-mar 351
}