Subversion Repositories oidplus

Rev

Rev 1300 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 1300 Rev 1301
1
<?php
1
<?php
2
 
2
 
3
/*
3
/*
4
 * OIDplus 2.0
4
 * OIDplus 2.0
5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
6
 *
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with 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
9
 * You may obtain a copy of the License at
10
 *
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
17
 * limitations under the License.
18
 */
18
 */
19
 
19
 
20
namespace ViaThinkSoft\OIDplus;
20
namespace ViaThinkSoft\OIDplus;
21
 
21
 
22
// phpcs:disable PSR1.Files.SideEffects
22
// phpcs:disable PSR1.Files.SideEffects
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
 
58
        /**
53
        /**
59
         * @param string $name
54
         * @param string $name
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)));
101
                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)
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)
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;
113
        }
136
        }
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';
124
                } else {
148
                } else {
125
                        $loginfo = 'into existing PHP session';
149
                        $loginfo = 'into existing PHP session';
126
                }
150
                }
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';
137
                } else {
162
                } else {
138
                        $loginfo = 'into existing PHP session';
163
                        $loginfo = 'into existing PHP session';
139
                }
164
                }
140
        }
165
        }
141
 
166
 
142
        /**
167
        /**
143
         * @param string $email
168
         * @param string $email
144
         * @param string $loginfo
169
         * @param string $loginfo
145
         * @return void
170
         * @return void
146
         */
171
         */
147
        public function raLogoutEx(string $email, string &$loginfo) {
172
        public function raLogoutEx(string $email, string &$loginfo) {
148
                $this->raLogout($email);
173
                $this->raLogout($email);
149
                $loginfo = 'from PHP session';
174
                $loginfo = 'from PHP session';
150
        }
175
        }
151
 
176
 
152
        /**
177
        /**
153
         * @param string $loginfo
178
         * @param string $loginfo
154
         * @return void
179
         * @return void
155
         */
180
         */
156
        public function adminLogoutEx(string &$loginfo) {
181
        public function adminLogoutEx(string &$loginfo) {
157
                $this->adminLogout();
182
                $this->adminLogout();
158
                $loginfo = 'from PHP session';
183
                $loginfo = 'from PHP session';
159
        }
184
        }
160
 
185
 
161
        /**
186
        /**
162
         * @return void
187
         * @return void
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
}
169
 
352