Subversion Repositories oidplus

Rev

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

Rev 1301 Rev 1303
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
                        // User is already logged in (a session or JWT exists), so we modify their login status
202
                        $acs->raLoginEx($email, $loginfo);
202
                        $acs->raLoginEx($email, $loginfo);
203
                        $acs->activate();
203
                        $acs->activate();
204
                } 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.)
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.)
206
                        if ($remember_me) {
206
                        if ($remember_me) {
207
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
207
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
208
                                        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'));
209
                                }
209
                                }
210
                                $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);
211
                                $authSimulation = new OIDplusAuthContentStoreJWT();
211
                                $newAuthStore = new OIDplusAuthContentStoreJWT();
212
                                $authSimulation->raLoginEx($email, $loginfo);
-
 
213
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
212
                                $newAuthStore->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
214
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
213
                                $newAuthStore->setValue('exp', time()+$ttl); // JWT "exp" attribute
215
                                $authSimulation->activate();
-
 
216
                        } else {
214
                        } else {
217
                                $authSimulation = new OIDplusAuthContentStoreSession();
215
                                $newAuthStore = new OIDplusAuthContentStoreSession();
218
                                $authSimulation->raLoginEx($email, $loginfo);
-
 
219
                                $authSimulation->activate();
-
 
220
                        }
216
                        }
-
 
217
                        $newAuthStore->raLoginEx($email, $loginfo);
-
 
218
                        $newAuthStore->activate();
221
                }
219
                }
222
                $logmsg = "RA '$email' logged in";
220
                $logmsg = "RA '$email' logged in";
223
                if ($origin != '') $logmsg .= " via $origin";
221
                if ($origin != '') $logmsg .= " via $origin";
224
                if ($loginfo != '') $logmsg .= " ($loginfo)";
222
                if ($loginfo != '') $logmsg .= " ($loginfo)";
225
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
223
                OIDplus::logger()->log("V2:[OK]RA(%1)", "%2", $email, $logmsg);
226
        }
224
        }
227
 
225
 
228
        /**
226
        /**
229
         * @param string $email
227
         * @param string $email
230
         * @return void
228
         * @return void
231
         * @throws OIDplusException
229
         * @throws OIDplusException
232
         */
230
         */
233
        public function raLogoutEx(string $email) {
231
        public function raLogoutEx(string $email) {
234
                $loginfo = '';
232
                $loginfo = '';
235
 
233
 
236
                $acs = $this->getAuthContentStore();
234
                $acs = $this->getAuthContentStore();
237
                if (is_null($acs)) return;
235
                if (is_null($acs)) return;
238
                $acs->raLogoutEx($email, $loginfo);
236
                $acs->raLogoutEx($email, $loginfo);
239
 
237
 
240
                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);
241
 
239
 
242
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
240
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
243
                        // 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
244
                        $acs->destroySession();
242
                        $acs->destroySession();
245
                } else {
243
                } else {
246
                        // Get a new token for the remaining users
244
                        // Get a new token for the remaining users
247
                        $acs->activate();
245
                        $acs->activate();
248
                }
246
                }
249
        }
247
        }
250
 
248
 
251
        // Admin authentication functions
249
        // Admin authentication functions
252
 
250
 
253
        /**
251
        /**
254
         * @return void
252
         * @return void
255
         * @throws OIDplusException
253
         * @throws OIDplusException
256
         */
254
         */
257
        public function adminLogin() {
255
        public function adminLogin() {
258
                $acs = $this->getAuthContentStore();
256
                $acs = $this->getAuthContentStore();
259
                if (is_null($acs)) return;
257
                if (is_null($acs)) return;
260
                $acs->adminLogin();
258
                $acs->adminLogin();
261
        }
259
        }
262
 
260
 
263
        /**
261
        /**
264
         * @return void
262
         * @return void
265
         * @throws OIDplusException
263
         * @throws OIDplusException
266
         */
264
         */
267
        public function adminLogout() {
265
        public function adminLogout() {
268
                $acs = $this->getAuthContentStore();
266
                $acs = $this->getAuthContentStore();
269
                if (is_null($acs)) return;
267
                if (is_null($acs)) return;
270
                $acs->adminLogout();
268
                $acs->adminLogout();
271
        }
269
        }
272
 
270
 
273
        /**
271
        /**
274
         * @param string $password
272
         * @param string $password
275
         * @return bool
273
         * @return bool
276
         * @throws OIDplusException
274
         * @throws OIDplusException
277
         */
275
         */
278
        public function adminCheckPassword(string $password): bool {
276
        public function adminCheckPassword(string $password): bool {
279
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
277
                $cfgData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
280
                if (empty($cfgData)) {
278
                if (empty($cfgData)) {
281
                        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'));
282
                }
280
                }
283
 
281
 
284
                if (!is_array($cfgData)) {
282
                if (!is_array($cfgData)) {
285
                        $passwordDataArray = array($cfgData);
283
                        $passwordDataArray = array($cfgData);
286
                } else {
284
                } else {
287
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
285
                        $passwordDataArray = $cfgData; // Multiple Administrator passwords
288
                }
286
                }
289
 
287
 
290
                foreach ($passwordDataArray as $passwordData) {
288
                foreach ($passwordDataArray as $passwordData) {
291
                        if (str_starts_with($passwordData, '$')) {
289
                        if (str_starts_with($passwordData, '$')) {
292
                                // Version 3: BCrypt (or any other crypt)
290
                                // Version 3: BCrypt (or any other crypt)
293
                                $ok = password_verify($password, $passwordData);
291
                                $ok = password_verify($password, $passwordData);
294
                        } else if (strpos($passwordData, '$') !== false) {
292
                        } else if (strpos($passwordData, '$') !== false) {
295
                                // Version 2: SHA3-512 with salt
293
                                // Version 2: SHA3-512 with salt
296
                                list($salt, $hash) = explode('$', $passwordData, 2);
294
                                list($salt, $hash) = explode('$', $passwordData, 2);
297
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
295
                                $ok = hash_equals(sha3_512($salt.$password, true), base64_decode($hash));
298
                        } else {
296
                        } else {
299
                                // Version 1: SHA3-512 without salt
297
                                // Version 1: SHA3-512 without salt
300
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
298
                                $ok = hash_equals(sha3_512($password, true), base64_decode($passwordData));
301
                        }
299
                        }
302
                        if ($ok) return true;
300
                        if ($ok) return true;
303
                }
301
                }
304
 
302
 
305
                return false;
303
                return false;
306
        }
304
        }
307
 
305
 
308
        /**
306
        /**
309
         * @return bool
307
         * @return bool
310
         * @throws OIDplusException
308
         * @throws OIDplusException
311
         */
309
         */
312
        public function isAdminLoggedIn(): bool {
310
        public function isAdminLoggedIn(): bool {
313
                if ($this->forceAllLoggedOut()) {
311
                if ($this->forceAllLoggedOut()) {
314
                        return false;
312
                        return false;
315
                } else {
313
                } else {
316
                        $acs = $this->getAuthContentStore();
314
                        $acs = $this->getAuthContentStore();
317
                        if (is_null($acs)) return false;
315
                        if (is_null($acs)) return false;
318
                        return $acs->isAdminLoggedIn();
316
                        return $acs->isAdminLoggedIn();
319
                }
317
                }
320
        }
318
        }
321
 
319
 
322
        /**
320
        /**
323
         * "High level" function including logging and checking for valid JWT alternations
321
         * "High level" function including logging and checking for valid JWT alternations
324
         * @param bool $remember_me
322
         * @param bool $remember_me
325
         * @param string $origin
323
         * @param string $origin
326
         * @return void
324
         * @return void
327
         * @throws OIDplusException
325
         * @throws OIDplusException
328
         */
326
         */
329
        public function adminLoginEx(bool $remember_me, string $origin='') {
327
        public function adminLoginEx(bool $remember_me, string $origin='') {
330
                $loginfo = '';
328
                $loginfo = '';
331
                $acs = $this->getAuthContentStore();
329
                $acs = $this->getAuthContentStore();
332
                if (!is_null($acs)) {
330
                if (!is_null($acs)) {
333
                        // User is already logged in (a session or JWT exists), so we modify their login status
331
                        // User is already logged in (a session or JWT exists), so we modify their login status
334
                        $acs->adminLoginEx($loginfo);
332
                        $acs->adminLoginEx($loginfo);
335
                        $acs->activate();
333
                        $acs->activate();
336
                } else {
334
                } 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.)
335
                        // 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.)
338
                        if ($remember_me) {
336
                        if ($remember_me) {
339
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
337
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
340
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
338
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
341
                                }
339
                                }
342
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
340
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
343
                                $authSimulation = new OIDplusAuthContentStoreJWT();
341
                                $newAuthStore = new OIDplusAuthContentStoreJWT();
344
                                $authSimulation->adminLoginEx($loginfo);
-
 
345
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
342
                                $newAuthStore->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
346
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
343
                                $newAuthStore->setValue('exp', time()+$ttl); // JWT "exp" attribute
347
                                $authSimulation->activate();
-
 
348
                        } else {
344
                        } else {
349
                                $authSimulation = new OIDplusAuthContentStoreSession();
345
                                $newAuthStore = new OIDplusAuthContentStoreSession();
350
                                $authSimulation->adminLoginEx($loginfo);
-
 
351
                                $authSimulation->activate();
-
 
352
                        }
346
                        }
-
 
347
                        $newAuthStore->adminLoginEx($loginfo);
-
 
348
                        $newAuthStore->activate();
353
                }
349
                }
354
                $logmsg = "Admin logged in";
350
                $logmsg = "Admin logged in";
355
                if ($origin != '') $logmsg .= " via $origin";
351
                if ($origin != '') $logmsg .= " via $origin";
356
                if ($loginfo != '') $logmsg .= " ($loginfo)";
352
                if ($loginfo != '') $logmsg .= " ($loginfo)";
357
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
353
                OIDplus::logger()->log("V2:[OK]A", "%1", $logmsg);
358
        }
354
        }
359
 
355
 
360
        /**
356
        /**
361
         * @return void
357
         * @return void
362
         * @throws OIDplusException
358
         * @throws OIDplusException
363
         */
359
         */
364
        public function adminLogoutEx() {
360
        public function adminLogoutEx() {
365
                $loginfo = '';
361
                $loginfo = '';
366
 
362
 
367
                $acs = $this->getAuthContentStore();
363
                $acs = $this->getAuthContentStore();
368
                if (is_null($acs)) return;
364
                if (is_null($acs)) return;
369
                $acs->adminLogoutEx($loginfo);
365
                $acs->adminLogoutEx($loginfo);
370
 
366
 
371
                if ($this->raNumLoggedIn() == 0) {
367
                if ($this->raNumLoggedIn() == 0) {
372
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
368
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
373
                        $acs->destroySession();
369
                        $acs->destroySession();
374
                } else {
370
                } else {
375
                        // Get a new token for the remaining users
371
                        // Get a new token for the remaining users
376
                        $acs->activate();
372
                        $acs->activate();
377
                }
373
                }
378
 
374
 
379
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
375
                OIDplus::logger()->log("V2:[OK]A", "Admin logged out (%1)", $loginfo);
380
        }
376
        }
381
 
377
 
382
        // 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)
383
 
379
 
384
        /**
380
        /**
385
         * @param array|string $data
381
         * @param array|string $data
386
         * @return string
382
         * @return string
387
         * @throws OIDplusException
383
         * @throws OIDplusException
388
         */
384
         */
389
        public function makeSecret($data): string {
385
        public function makeSecret($data): string {
390
                if (!is_array($data)) $data = [$data];
386
                if (!is_array($data)) $data = [$data];
391
                $data = json_encode($data);
387
                $data = json_encode($data);
392
                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);
393
        }
389
        }
394
 
390
 
395
        /**
391
        /**
396
         * @param array|string $data Arbitary data to be validated later
392
         * @param array|string $data Arbitary data to be validated later
397
         * @return string A string that need to be validated with validateAuthKey
393
         * @return string A string that need to be validated with validateAuthKey
398
         * @throws OIDplusException
394
         * @throws OIDplusException
399
         */
395
         */
400
        public function makeAuthKey($data): string {
396
        public function makeAuthKey($data): string {
401
                if (!is_array($data)) $data = [$data];
397
                if (!is_array($data)) $data = [$data];
402
                $ts = time();
398
                $ts = time();
403
                $data_ext = [$ts, $data];
399
                $data_ext = [$ts, $data];
404
                $secret = $this->makeSecret($data_ext);
400
                $secret = $this->makeSecret($data_ext);
405
                return $ts.'.'.$secret;
401
                return $ts.'.'.$secret;
406
        }
402
        }
407
 
403
 
408
        /**
404
        /**
409
         * @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()
410
         * @param string $auth_key The result from makeAuthKey()
406
         * @param string $auth_key The result from makeAuthKey()
411
         * @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)
412
         * @return bool True if the key is valid and not expired.
408
         * @return bool True if the key is valid and not expired.
413
         * @throws OIDplusException
409
         * @throws OIDplusException
414
         */
410
         */
415
        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 {
416
                $auth_key_ary = explode('.', $auth_key, 2);
412
                $auth_key_ary = explode('.', $auth_key, 2);
417
                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
418
                list($ts, $secret) = $auth_key_ary;
414
                list($ts, $secret) = $auth_key_ary;
419
                if (!is_numeric($ts)) return false; // invalid auth key syntax
415
                if (!is_numeric($ts)) return false; // invalid auth key syntax
420
                if ($valid_secs >= 0) {
416
                if ($valid_secs >= 0) {
421
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
417
                        if (time() > ($ts+$valid_secs)) return false; // expired auth key
422
                }
418
                }
423
                if (!is_array($data)) $data = [$data];
419
                if (!is_array($data)) $data = [$data];
424
                $data_ext = [(int)$ts, $data];
420
                $data_ext = [(int)$ts, $data];
425
                return hash_equals($this->makeSecret($data_ext), $secret);
421
                return hash_equals($this->makeSecret($data_ext), $secret);
426
        }
422
        }
427
 
423
 
428
        // "Veto" functions to force logout state
424
        // "Veto" functions to force logout state
429
 
425
 
430
        /**
426
        /**
431
         * @return bool
427
         * @return bool
432
         */
428
         */
433
        protected function forceAllLoggedOut(): bool {
429
        protected function forceAllLoggedOut(): bool {
434
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
430
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
435
                        // The sitemap may not contain any confidential information,
431
                        // The sitemap may not contain any confidential information,
436
                        // even if the user is logged in, because the admin could
432
                        // even if the user is logged in, because the admin could
437
                        // accidentally copy-paste the sitemap to a
433
                        // accidentally copy-paste the sitemap to a
438
                        // search engine control panel while they are logged in
434
                        // search engine control panel while they are logged in
439
                        return true;
435
                        return true;
440
                } else {
436
                } else {
441
                        return false;
437
                        return false;
442
                }
438
                }
443
        }
439
        }
444
 
440
 
445
        // CSRF functions
441
        // CSRF functions
446
 
442
 
447
        private $enable_csrf = true;
443
        private $enable_csrf = true;
448
 
444
 
449
        /**
445
        /**
450
         * @return void
446
         * @return void
451
         */
447
         */
452
        public function enableCSRF() {
448
        public function enableCSRF() {
453
                $this->enable_csrf = true;
449
                $this->enable_csrf = true;
454
        }
450
        }
455
 
451
 
456
        /**
452
        /**
457
         * @return void
453
         * @return void
458
         */
454
         */
459
        public function disableCSRF() {
455
        public function disableCSRF() {
460
                $this->enable_csrf = false;
456
                $this->enable_csrf = false;
461
        }
457
        }
462
 
458
 
463
        /**
459
        /**
464
         * @return string
460
         * @return string
465
         * @throws \Random\RandomException
461
         * @throws \Random\RandomException
466
         */
462
         */
467
        public function genCSRFToken(): string {
463
        public function genCSRFToken(): string {
468
                return random_bytes_ex(64, false, false);
464
                return random_bytes_ex(64, false, false);
469
        }
465
        }
470
 
466
 
471
        /**
467
        /**
472
         * @return void
468
         * @return void
473
         * @throws OIDplusException
469
         * @throws OIDplusException
474
         */
470
         */
475
        public function checkCSRF() {
471
        public function checkCSRF() {
476
                if (!$this->enable_csrf) return;
472
                if (!$this->enable_csrf) return;
477
 
473
 
478
                $request_token = $_REQUEST['csrf_token'] ?? '';
474
                $request_token = $_REQUEST['csrf_token'] ?? '';
479
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
475
                $cookie_token = $_COOKIE['csrf_token'] ?? '';
480
 
476
 
481
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
477
                if (empty($request_token) || empty($cookie_token) || ($request_token !== $cookie_token)) {
482
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
478
                        if (OIDplus::baseConfig()->getValue('DEBUG')) {
483
                                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',
484
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
480
                                        isset($_REQUEST['csrf_token']) ? '"'.$_REQUEST['csrf_token'].'"' : 'NULL',
485
                                        $_COOKIE['csrf_token'] ?? 'NULL'
481
                                        $_COOKIE['csrf_token'] ?? 'NULL'
486
                                ));
482
                                ));
487
                        } else {
483
                        } else {
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.'));
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.'));
489
                        }
485
                        }
490
                }
486
                }
491
        }
487
        }
492
 
488
 
493
        // Generate RA passwords
489
        // Generate RA passwords
494
 
490
 
495
        /**
491
        /**
496
         * @param string $password
492
         * @param string $password
497
         * @return OIDplusRAAuthInfo
493
         * @return OIDplusRAAuthInfo
498
         * @throws OIDplusException
494
         * @throws OIDplusException
499
         */
495
         */
500
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
496
        public function raGeneratePassword(string $password): OIDplusRAAuthInfo {
501
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
497
                $plugin = OIDplus::getDefaultRaAuthPlugin(true);
502
                return $plugin->generate($this->raPepperProcessing($password));
498
                return $plugin->generate($this->raPepperProcessing($password));
503
        }
499
        }
504
 
500
 
505
        // Generate admin password
501
        // Generate admin password
506
 
502
 
507
        /* 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 */
508
 
504
 
509
}
505
}
510
 
506