Subversion Repositories oidplus

Rev

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

Rev 1283 Rev 1300
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 OIDplusAuthUtils extends OIDplusBaseClass {
26
class OIDplusAuthUtils extends OIDplusBaseClass {
27
 
27
 
28
        // Useful functions
28
        // Useful functions
29
 
29
 
30
        /**
30
        /**
31
         * @param string $password
31
         * @param string $password
32
         * @return string
32
         * @return string
33
         * @throws OIDplusException
33
         * @throws OIDplusException
34
         */
34
         */
35
        private function raPepperProcessing(string $password): string {
35
        private function raPepperProcessing(string $password): string {
36
                // Additional feature: Pepper
36
                // Additional feature: Pepper
37
                // The pepper is stored inside the base configuration file
37
                // The pepper is stored inside the base configuration file
38
                // It prevents that an attacker with SQL write rights can
38
                // It prevents that an attacker with SQL write rights can
39
                // create accounts.
39
                // create accounts.
40
                // ATTENTION!!! If a pepper is used, then the
40
                // ATTENTION!!! If a pepper is used, then the
41
                // hashes are bound to that pepper. If you change the pepper,
41
                // hashes are bound to that pepper. If you change the pepper,
42
                // then ALL passwords of RAs become INVALID!
42
                // then ALL passwords of RAs become INVALID!
43
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
43
                $pepper = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER','');
44
                if ($pepper !== '') {
44
                if ($pepper !== '') {
45
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
45
                        $algo = OIDplus::baseConfig()->getValue('RA_PASSWORD_PEPPER_ALGO','sha512'); // sha512 works with PHP 7.0
46
                        if (strtolower($algo) === 'sha3-512') {
46
                        if (strtolower($algo) === 'sha3-512') {
47
                                $hmac = sha3_512_hmac($password, $pepper);
47
                                $hmac = sha3_512_hmac($password, $pepper);
48
                        } else {
48
                        } else {
49
                                $hmac = hash_hmac($algo, $password, $pepper);
49
                                $hmac = hash_hmac($algo, $password, $pepper);
50
                        }
50
                        }
51
                        if ($hmac === "") throw new OIDplusException(_L('HMAC failed'));
51
                        if ($hmac === "") throw new OIDplusException(_L('HMAC failed'));
52
                        return $hmac;
52
                        return $hmac;
53
                } else {
53
                } else {
54
                        return $password;
54
                        return $password;
55
                }
55
                }
56
        }
56
        }
57
 
57
 
58
        // Content provider
58
        // Content provider
59
 
59
 
60
        /**
60
        /**
61
         * @return string
61
         * @return string
62
         * @throws OIDplusException
62
         * @throws OIDplusException
63
         */
63
         */
64
        public function getAuthMethod(): string {
64
        public function getAuthMethod(): string {
65
                $acs = $this->getAuthContentStore();
65
                $acs = $this->getAuthContentStore();
66
                if (is_null($acs)) return 'null';
66
                if (is_null($acs)) return 'null';
67
                return get_class($acs);
67
                return get_class($acs);
68
        }
68
        }
69
 
69
 
70
        /**
70
        /**
71
         * @return OIDplusAuthContentStore|null
71
         * @return OIDplusAuthContentStore|null
72
         * @throws OIDplusException
72
         * @throws OIDplusException
73
         */
73
         */
74
        protected function getAuthContentStore()/*: ?OIDplusAuthContentStore*/ {
74
        protected function getAuthContentStore()/*: ?OIDplusAuthContentStore*/ {
75
                // TODO: Should we implement these AuthContentStore as plugin type, so that there can be more than just JWT and PHP session?
75
                // TODO: Should we implement these AuthContentStore as plugin type, so that there can be more than just JWT and PHP session?
76
 
76
 
77
                // Logged in via JWT
77
                // Logged in via JWT
-
 
78
                // (The JWT can come from a REST Authentication Bearer, an AJAX Cookie, or an Automated AJAX Call GET/POST token.)
78
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
79
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
79
                if ($tmp) return $tmp;
80
                if ($tmp) return $tmp;
80
 
81
 
81
                // For REST, we must only allow JWT from Bearer and nothing else! So disable cookies if we are accessing the REST plugin
-
 
82
                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
-
 
83
                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)
-
 
84
 
-
 
85
                        // Normal login via web-browser
82
                // Normal login via web-browser
86
                        // Cookie will only be created once content is stored
83
                // Cookie will only be created once content is stored
87
                        $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
84
                $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
88
                        if ($tmp) return $tmp;
85
                if ($tmp) return $tmp;
89
 
86
 
90
                }
-
 
91
 
-
 
92
                // No active session and no JWT token available. User is not logged in.
87
                // No active session and no JWT token available. User is not logged in.
93
                return null;
88
                return null;
94
        }
89
        }
95
 
90
 
96
        /**
91
        /**
97
         * @param string $name
92
         * @param string $name
98
         * @param mixed|null $default
93
         * @param mixed|null $default
99
         * @return mixed
94
         * @return mixed
100
         * @throws OIDplusException
95
         * @throws OIDplusException
101
         */
96
         */
102
        public function getExtendedAttribute(string $name, $default=NULL) {
97
        public function getExtendedAttribute(string $name, $default=NULL) {
103
                $acs = $this->getAuthContentStore();
98
                $acs = $this->getAuthContentStore();
104
                if (is_null($acs)) return $default;
99
                if (is_null($acs)) return $default;
105
                return $acs->getValue($name, $default);
100
                return $acs->getValue($name, $default);
106
        }
101
        }
107
 
102
 
108
        // RA authentication functions
103
        // RA authentication functions
109
 
104
 
110
        /**
105
        /**
111
         * @param string $email
106
         * @param string $email
112
         * @return void
107
         * @return void
113
         * @throws OIDplusException
108
         * @throws OIDplusException
114
         */
109
         */
115
        public function raLogin(string $email) {
110
        public function raLogin(string $email) {
116
                $acs = $this->getAuthContentStore();
111
                $acs = $this->getAuthContentStore();
117
                if (is_null($acs)) return;
112
                if (is_null($acs)) return;
118
                $acs->raLogin($email);
113
                $acs->raLogin($email);
119
        }
114
        }
120
 
115
 
121
        /**
116
        /**
122
         * @param string $email
117
         * @param string $email
123
         * @return void
118
         * @return void
124
         * @throws OIDplusException
119
         * @throws OIDplusException
125
         */
120
         */
126
        public function raLogout(string $email) {
121
        public function raLogout(string $email) {
127
                $acs = $this->getAuthContentStore();
122
                $acs = $this->getAuthContentStore();
128
                if (is_null($acs)) return;
123
                if (is_null($acs)) return;
129
                $acs->raLogout($email);
124
                $acs->raLogout($email);
130
        }
125
        }
131
 
126
 
132
        /**
127
        /**
133
         * @param string $ra_email
128
         * @param string $ra_email
134
         * @param string $password
129
         * @param string $password
135
         * @return bool
130
         * @return bool
136
         * @throws OIDplusException
131
         * @throws OIDplusException
137
         */
132
         */
138
        public function raCheckPassword(string $ra_email, string $password): bool {
133
        public function raCheckPassword(string $ra_email, string $password): bool {
139
                $ra = new OIDplusRA($ra_email);
134
                $ra = new OIDplusRA($ra_email);
140
 
135
 
141
                // Get RA info from RA
136
                // Get RA info from RA
142
                $authInfo = $ra->getAuthInfo();
137
                $authInfo = $ra->getAuthInfo();
143
                if (!$authInfo) return false; // user not found
138
                if (!$authInfo) return false; // user not found
144
 
139
 
145
                // Ask plugins if they can verify this hash
140
                // Ask plugins if they can verify this hash
146
                $plugins = OIDplus::getAuthPlugins();
141
                $plugins = OIDplus::getAuthPlugins();
147
                if (count($plugins) == 0) {
142
                if (count($plugins) == 0) {
148
                        throw new OIDplusException(_L('No RA authentication plugins found'));
143
                        throw new OIDplusException(_L('No RA authentication plugins found'));
149
                }
144
                }
150
                foreach ($plugins as $plugin) {
145
                foreach ($plugins as $plugin) {
151
                        if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true;
146
                        if ($plugin->verify($authInfo, $this->raPepperProcessing($password))) return true;
152
                }
147
                }
153
 
148
 
154
                return false;
149
                return false;
155
        }
150
        }
156
 
151
 
157
        /**
152
        /**
158
         * @return int
153
         * @return int
159
         * @throws OIDplusException
154
         * @throws OIDplusException
160
         */
155
         */
161
        public function raNumLoggedIn(): int {
156
        public function raNumLoggedIn(): int {
162
                $acs = $this->getAuthContentStore();
157
                $acs = $this->getAuthContentStore();
163
                if (is_null($acs)) return 0;
158
                if (is_null($acs)) return 0;
164
                return $acs->raNumLoggedIn();
159
                return $acs->raNumLoggedIn();
165
        }
160
        }
166
 
161
 
167
        /**
162
        /**
168
         * @return OIDplusRA[]
163
         * @return OIDplusRA[]
169
         * @throws OIDplusException
164
         * @throws OIDplusException
170
         */
165
         */
171
        public function loggedInRaList(): array {
166
        public function loggedInRaList(): array {
172
                if ($this->forceAllLoggedOut()) {
167
                if ($this->forceAllLoggedOut()) {
173
                        return array();
168
                        return array();
174
                } else {
169
                } else {
175
                        $acs = $this->getAuthContentStore();
170
                        $acs = $this->getAuthContentStore();
176
                        if (is_null($acs)) return array();
171
                        if (is_null($acs)) return array();
177
                        return $acs->loggedInRaList();
172
                        return $acs->loggedInRaList();
178
                }
173
                }
179
        }
174
        }
180
 
175
 
181
        /**
176
        /**
182
         * @param string|OIDplusRA $ra
177
         * @param string|OIDplusRA $ra
183
         * @return bool
178
         * @return bool
184
         * @throws OIDplusException
179
         * @throws OIDplusException
185
         */
180
         */
186
        public function isRaLoggedIn($ra): bool {
181
        public function isRaLoggedIn($ra): bool {
187
                $email = $ra instanceof OIDplusRA ? $ra->raEmail() : $ra;
182
                $email = $ra instanceof OIDplusRA ? $ra->raEmail() : $ra;
188
                $acs = $this->getAuthContentStore();
183
                $acs = $this->getAuthContentStore();
189
                if (is_null($acs)) return false;
184
                if (is_null($acs)) return false;
190
                return $acs->isRaLoggedIn($email);
185
                return $acs->isRaLoggedIn($email);
191
        }
186
        }
192
 
187
 
193
        // "High level" function including logging and checking for valid JWT alternations
188
        // "High level" function including logging and checking for valid JWT alternations
194
 
189
 
195
        /**
190
        /**
196
         * @param string $email
191
         * @param string $email
197
         * @param bool $remember_me
192
         * @param bool $remember_me
198
         * @param string $origin
193
         * @param string $origin
199
         * @return void
194
         * @return void
200
         * @throws OIDplusException
195
         * @throws OIDplusException
201
         */
196
         */
202
        public function raLoginEx(string $email, bool $remember_me, string $origin='') {
197
        public function raLoginEx(string $email, bool $remember_me, string $origin='') {
203
                $loginfo = '';
198
                $loginfo = '';
204
                $acs = $this->getAuthContentStore();
199
                $acs = $this->getAuthContentStore();
205
                if (!is_null($acs)) {
200
                if (!is_null($acs)) {
206
                        $acs->raLoginEx($email, $loginfo);
201
                        $acs->raLoginEx($email, $loginfo);
207
                        $acs->activate();
202
                        $acs->activate();
208
                } else {
203
                } else {
209
                        if ($remember_me) {
204
                        if ($remember_me) {
210
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
205
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
211
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
206
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
212
                                }
207
                                }
213
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
208
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
214
                                $authSimulation = new OIDplusAuthContentStoreJWT();
209
                                $authSimulation = new OIDplusAuthContentStoreJWT();
215
                                $authSimulation->raLoginEx($email, $loginfo);
210
                                $authSimulation->raLoginEx($email, $loginfo);
216
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
211
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
217
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
212
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
218
                                $authSimulation->activate();
213
                                $authSimulation->activate();
219
                        } else {
214
                        } else {
220
                                $authSimulation = new OIDplusAuthContentStoreSession();
215
                                $authSimulation = new OIDplusAuthContentStoreSession();
221
                                $authSimulation->raLoginEx($email, $loginfo);
216
                                $authSimulation->raLoginEx($email, $loginfo);
222
                                $authSimulation->activate();
217
                                $authSimulation->activate();
223
                        }
218
                        }
224
                }
219
                }
225
                $logmsg = "RA '$email' logged in";
220
                $logmsg = "RA '$email' logged in";
226
                if ($origin != '') $logmsg .= " via $origin";
221
                if ($origin != '') $logmsg .= " via $origin";
227
                if ($loginfo != '') $logmsg .= " ($loginfo)";
222
                if ($loginfo != '') $logmsg .= " ($loginfo)";
228
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
223
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
229
        }
224
        }
230
 
225
 
231
        /**
226
        /**
232
         * @param string $email
227
         * @param string $email
233
         * @return void
228
         * @return void
234
         * @throws OIDplusException
229
         * @throws OIDplusException
235
         */
230
         */
236
        public function raLogoutEx(string $email) {
231
        public function raLogoutEx(string $email) {
237
                $loginfo = '';
232
                $loginfo = '';
238
 
233
 
239
                $acs = $this->getAuthContentStore();
234
                $acs = $this->getAuthContentStore();
240
                if (is_null($acs)) return;
235
                if (is_null($acs)) return;
241
                $acs->raLogoutEx($email, $loginfo);
236
                $acs->raLogoutEx($email, $loginfo);
242
 
237
 
243
                OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo);
238
                OIDplus::logger()->log("V2:[OK]RA(%1)", "RA '%1' logged out (%2)", $email, $loginfo);
244
 
239
 
245
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
240
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
246
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
241
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
247
                        $acs->destroySession();
242
                        $acs->destroySession();
248
                } else {
243
                } else {
249
                        // Get a new token for the remaining users
244
                        // Get a new token for the remaining users
250
                        $acs->activate();
245
                        $acs->activate();
251
                }
246
                }
252
        }
247
        }
253
 
248
 
254
        // Admin authentication functions
249
        // Admin authentication functions
255
 
250
 
256
        /**
251
        /**
257
         * @return void
252
         * @return void
258
         * @throws OIDplusException
253
         * @throws OIDplusException
259
         */
254
         */
260
        public function adminLogin() {
255
        public function adminLogin() {
261
                $acs = $this->getAuthContentStore();
256
                $acs = $this->getAuthContentStore();
262
                if (is_null($acs)) return;
257
                if (is_null($acs)) return;
263
                $acs->adminLogin();
258
                $acs->adminLogin();
264
        }
259
        }
265
 
260
 
266
        /**
261
        /**
267
         * @return void
262
         * @return void
268
         * @throws OIDplusException
263
         * @throws OIDplusException
269
         */
264
         */
270
        public function adminLogout() {
265
        public function adminLogout() {
271
                $acs = $this->getAuthContentStore();
266
                $acs = $this->getAuthContentStore();
272
                if (is_null($acs)) return;
267
                if (is_null($acs)) return;
273
                $acs->adminLogout();
268
                $acs->adminLogout();
274
        }
269
        }
275
 
270
 
276
        /**
271
        /**
277
         * @param string $password
272
         * @param string $password
278
         * @return bool
273
         * @return bool
279
         * @throws OIDplusException
274
         * @throws OIDplusException
280
         */
275
         */
281
        public function adminCheckPassword(string $password): bool {
276
        public function adminCheckPassword(string $password): bool {
282
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
277
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
283
                if (empty($cfgData)) {
278
                if (empty($cfgData)) {
284
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
279
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
285
                }
280
                }
286
 
281
 
287
                if (!is_array($cfgData)) {
282
                if (!is_array($cfgData)) {
288
                        $passwordDataArray = array($cfgData);
283
                        $passwordDataArray = array($cfgData);
289
                } else {
284
                } else {
290
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
285
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
291
                }
286
                }
292
 
287
 
293
                foreach ($passwordDataArray as $passwordData) {
288
                foreach ($passwordDataArray as $passwordData) {
294
                        if (str_starts_with($passwordData, '$')) {
289
                        if (str_starts_with($passwordData, '$')) {
295
                                // Version 3: BCrypt (or any other crypt)
290
                                // Version 3: BCrypt (or any other crypt)
296
                                $ok = password_verify($password, $passwordData);
291
                                $ok = password_verify($password, $passwordData);
297
                        } else if (strpos($passwordData, '$') !== false) {
292
                        } else if (strpos($passwordData, '$') !== false) {
298
                                // Version 2: SHA3-512 with salt
293
                                // Version 2: SHA3-512 with salt
299
                                list($salt, $hash) = explode('$', $passwordData, 2);
294
                                list($salt, $hash) = explode('$', $passwordData, 2);
300
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
295
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
301
                        } else {
296
                        } else {
302
                                // Version 1: SHA3-512 without salt
297
                                // Version 1: SHA3-512 without salt
303
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
298
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
304
                        }
299
                        }
305
                        if ($ok) return true;
300
                        if ($ok) return true;
306
                }
301
                }
307
 
302
 
308
                return false;
303
                return false;
309
        }
304
        }
310
 
305
 
311
        /**
306
        /**
312
         * @return bool
307
         * @return bool
313
         * @throws OIDplusException
308
         * @throws OIDplusException
314
         */
309
         */
315
        public function isAdminLoggedIn(): bool {
310
        public function isAdminLoggedIn(): bool {
316
                if ($this->forceAllLoggedOut()) {
311
                if ($this->forceAllLoggedOut()) {
317
                        return false;
312
                        return false;
318
                } else {
313
                } else {
319
                        $acs = $this->getAuthContentStore();
314
                        $acs = $this->getAuthContentStore();
320
                        if (is_null($acs)) return false;
315
                        if (is_null($acs)) return false;
321
                        return $acs->isAdminLoggedIn();
316
                        return $acs->isAdminLoggedIn();
322
                }
317
                }
323
        }
318
        }
324
 
319
 
325
        /**
320
        /**
326
         * "High level" function including logging and checking for valid JWT alternations
321
         * "High level" function including logging and checking for valid JWT alternations
327
         * @param bool $remember_me
322
         * @param bool $remember_me
328
         * @param string $origin
323
         * @param string $origin
329
         * @return void
324
         * @return void
330
         * @throws OIDplusException
325
         * @throws OIDplusException
331
         */
326
         */
332
        public function adminLoginEx(bool $remember_me, string $origin='') {
327
        public function adminLoginEx(bool $remember_me, string $origin='') {
333
                $loginfo = '';
328
                $loginfo = '';
334
                $acs = $this->getAuthContentStore();
329
                $acs = $this->getAuthContentStore();
335
                if (!is_null($acs)) {
330
                if (!is_null($acs)) {
336
                        $acs->adminLoginEx($loginfo);
331
                        $acs->adminLoginEx($loginfo);
337
                        $acs->activate();
332
                        $acs->activate();
338
                } else {
333
                } else {
339
                        if ($remember_me) {
334
                        if ($remember_me) {
340
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
335
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
341
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
336
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
342
                                }
337
                                }
343
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
338
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
344
                                $authSimulation = new OIDplusAuthContentStoreJWT();
339
                                $authSimulation = new OIDplusAuthContentStoreJWT();
345
                                $authSimulation->adminLoginEx($loginfo);
340
                                $authSimulation->adminLoginEx($loginfo);
346
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
341
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
347
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
342
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
348
                                $authSimulation->activate();
343
                                $authSimulation->activate();
349
                        } else {
344
                        } else {
350
                                $authSimulation = new OIDplusAuthContentStoreSession();
345
                                $authSimulation = new OIDplusAuthContentStoreSession();
351
                                $authSimulation->adminLoginEx($loginfo);
346
                                $authSimulation->adminLoginEx($loginfo);
352
                                $authSimulation->activate();
347
                                $authSimulation->activate();
353
                        }
348
                        }
354
                }
349
                }
355
                $logmsg = "Admin logged in";
350
                $logmsg = "Admin logged in";
356
                if ($origin != '') $logmsg .= " via $origin";
351
                if ($origin != '') $logmsg .= " via $origin";
357
                if ($loginfo != '') $logmsg .= " ($loginfo)";
352
                if ($loginfo != '') $logmsg .= " ($loginfo)";
358
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
353
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
359
        }
354
        }
360
 
355
 
361
        /**
356
        /**
362
         * @return void
357
         * @return void
363
         * @throws OIDplusException
358
         * @throws OIDplusException
364
         */
359
         */
365
        public function adminLogoutEx() {
360
        public function adminLogoutEx() {
366
                $loginfo = '';
361
                $loginfo = '';
367
 
362
 
368
                $acs = $this->getAuthContentStore();
363
                $acs = $this->getAuthContentStore();
369
                if (is_null($acs)) return;
364
                if (is_null($acs)) return;
370
                $acs->adminLogoutEx($loginfo);
365
                $acs->adminLogoutEx($loginfo);
371
 
366
 
372
                if ($this->raNumLoggedIn() == 0) {
367
                if ($this->raNumLoggedIn() == 0) {
373
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
368
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
374
                        $acs->destroySession();
369
                        $acs->destroySession();
375
                } else {
370
                } else {
376
                        // Get a new token for the remaining users
371
                        // Get a new token for the remaining users
377
                        $acs->activate();
372
                        $acs->activate();
378
                }
373
                }
379
 
374
 
380
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
375
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
381
        }
376
        }
382
 
377
 
383
        // Authentication keys for generating secrets or validating arguments (e.g. sent by mail)
378
        // Authentication keys for generating secrets or validating arguments (e.g. sent by mail)
384
 
379
 
385
        /**
380
        /**
386
         * @param array|string $data
381
         * @param array|string $data
387
         * @return string
382
         * @return string
388
         * @throws OIDplusException
383
         * @throws OIDplusException
389
         */
384
         */
390
        public function makeSecret($data): string {
385
        public function makeSecret($data): string {
391
                if (!is_array($data)) $data = [$data];
386
                if (!is_array($data)) $data = [$data];
392
                $data = json_encode($data);
387
                $data = json_encode($data);
393
                return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
388
                return sha3_512_hmac($data, 'OIDplus:'.OIDplus::baseConfig()->getValue('SERVER_SECRET'), false);
394
        }
389
        }
395
 
390
 
396
        /**
391
        /**
397
         * @param array|string $data Arbitary data to be validated later
392
         * @param array|string $data Arbitary data to be validated later
398
         * @return string A string that need to be validated with validateAuthKey
393
         * @return string A string that need to be validated with validateAuthKey
399
         * @throws OIDplusException
394
         * @throws OIDplusException
400
         */
395
         */
401
        public function makeAuthKey($data): string {
396
        public function makeAuthKey($data): string {
402
                if (!is_array($data)) $data = [$data];
397
                if (!is_array($data)) $data = [$data];
403
                $ts = time();
398
                $ts = time();
404
                $data_ext = [$ts, $data];
399
                $data_ext = [$ts, $data];
405
                $secret = $this->makeSecret($data_ext);
400
                $secret = $this->makeSecret($data_ext);
406
                return $ts.'.'.$secret;
401
                return $ts.'.'.$secret;
407
        }
402
        }
408
 
403
 
409
        /**
404
        /**
410
         * @param array|string $data The original data that had been passed to makeAuthKey()
405
         * @param array|string $data The original data that had been passed to makeAuthKey()
411
         * @param string $auth_key The result from makeAuthKey()
406
         * @param string $auth_key The result from makeAuthKey()
412
         * @param int $valid_secs How many seconds is the auth key valid? (-1 for infinite)
407
         * @param int $valid_secs How many seconds is the auth key valid? (-1 for infinite)
413
         * @return bool True if the key is valid and not expired.
408
         * @return bool True if the key is valid and not expired.
414
         * @throws OIDplusException
409
         * @throws OIDplusException
415
         */
410
         */
416
        public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool {
411
        public function validateAuthKey($data, string $auth_key, int $valid_secs=-1): bool {
417
                $auth_key_ary = explode('.', $auth_key, 2);
412
                $auth_key_ary = explode('.', $auth_key, 2);
418
                if (count($auth_key_ary) != 2) return false; // invalid auth key syntax
413
                if (count($auth_key_ary) != 2) return false; // invalid auth key syntax
419
                list($ts, $secret) = $auth_key_ary;
414
                list($ts, $secret) = $auth_key_ary;
420
                if (!is_numeric($ts)) return false; // invalid auth key syntax
415
                if (!is_numeric($ts)) return false; // invalid auth key syntax
421
                if ($valid_secs >= 0) {
416
                if ($valid_secs >= 0) {
422
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
417
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
423
                }
418
                }
424
                if (!is_array($data)) $data = [$data];
419
                if (!is_array($data)) $data = [$data];
425
                $data_ext = [(int)$ts, $data];
420
                $data_ext = [(int)$ts, $data];
426
                return hash_equals($this->makeSecret($data_ext), $secret);
421
                return hash_equals($this->makeSecret($data_ext), $secret);
427
        }
422
        }
428
 
423
 
429
        // "Veto" functions to force logout state
424
        // "Veto" functions to force logout state
430
 
425
 
431
        /**
426
        /**
432
         * @return bool
427
         * @return bool
433
         */
428
         */
434
        protected function forceAllLoggedOut(): bool {
429
        protected function forceAllLoggedOut(): bool {
435
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
430
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
436
                        // The sitemap may not contain any confidential information,
431
                        // The sitemap may not contain any confidential information,
437
                        // even if the user is logged in, because the admin could
432
                        // even if the user is logged in, because the admin could
438
                        // accidentally copy-paste the sitemap to a
433
                        // accidentally copy-paste the sitemap to a
439
                        // search engine control panel while they are logged in
434
                        // search engine control panel while they are logged in
440
                        return true;
435
                        return true;
441
                } else {
436
                } else {
442
                        return false;
437
                        return false;
443
                }
438
                }
444
        }
439
        }
445
 
440
 
446
        // CSRF functions
441
        // CSRF functions
447
 
442
 
448
        private $enable_csrf = true;
443
        private $enable_csrf = true;
449
 
444
 
450
        /**
445
        /**
451
         * @return void
446
         * @return void
452
         */
447
         */
453
        public function enableCSRF() {
448
        public function enableCSRF() {
454
                $this->enable_csrf = true;
449
                $this->enable_csrf = true;
455
        }
450
        }
456
 
451
 
457
        /**
452
        /**
458
         * @return void
453
         * @return void
459
         */
454
         */
460
        public function disableCSRF() {
455
        public function disableCSRF() {
461
                $this->enable_csrf = false;
456
                $this->enable_csrf = false;
462
        }
457
        }
463
 
458
 
464
        /**
459
        /**
465
         * @return string
460
         * @return string
466
         * @throws \Random\RandomException
461
         * @throws \Random\RandomException
467
         */
462
         */
468
        public function genCSRFToken(): string {
463
        public function genCSRFToken(): string {
469
                return random_bytes_ex(64, false, false);
464
                return random_bytes_ex(64, false, false);
470
        }
465
        }
471
 
466
 
472
        /**
467
        /**
473
         * @return void
468
         * @return void
474
         * @throws OIDplusException
469
         * @throws OIDplusException
475
         */
470
         */
476
        public function checkCSRF() {
471
        public function checkCSRF() {
477
                if (!$this->enable_csrf) return;
472
                if (!$this->enable_csrf) return;
478
 
473
 
479
                $request_token = $_REQUEST['csrf_token'] ?? '';
474
                $request_token = $_REQUEST['csrf_token'] ?? '';
480
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
475
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
481
 
476
 
482
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
477
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
483
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
478
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
484
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
479
                                throw new OIDplusException(_L('Missing or wrong CSRF Token: Request %1 vs Cookie %2',
485
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
480
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
486
                                        $_COOKIE['csrf_token'] ?? 'NULL'
481
                                        $_COOKIE['csrf_token'] ?? 'NULL'
487
                                ));
482
                                ));
488
                        } else {
483
                        } else {
489
                                throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.'));
484
                                throw new OIDplusException(_L('Missing or wrong "CSRF Token". To fix the issue, try clearing your browser cache and reload the page. If you visited the page via HTTPS before, try HTTPS in case you are currently connected via HTTP.'));
490
                        }
485
                        }
491
                }
486
                }
492
        }
487
        }
493
 
488
 
494
        // Generate RA passwords
489
        // Generate RA passwords
495
 
490
 
496
        /**
491
        /**
497
         * @param string $password
492
         * @param string $password
498
         * @return OIDplusRAAuthInfo
493
         * @return OIDplusRAAuthInfo
499
         * @throws OIDplusException
494
         * @throws OIDplusException
500
         */
495
         */
501
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
496
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
502
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
497
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
503
                return $plugin->generate($this->raPepperProcessing($password));
498
                return $plugin->generate($this->raPepperProcessing($password));
504
        }
499
        }
505
 
500
 
506
        // Generate admin password
501
        // Generate admin password
507
 
502
 
508
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
503
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
509
 
504
 
510
}
505
}
511
 
506