Subversion Repositories oidplus

Rev

Rev 1339 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

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