Subversion Repositories oidplus

Rev

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