Subversion Repositories oidplus

Rev

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

Rev 1283 Rev 1298
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 OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy {
26
class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy {
27
 
27
 
28
        /**
28
        /**
29
         * Cookie name for the JWT auth token
29
         * Cookie name for the JWT auth token
30
         */
30
         */
31
        const COOKIE_NAME = 'OIDPLUS_AUTH_JWT';
31
        const COOKIE_NAME = 'OIDPLUS_AUTH_JWT';
32
 
32
 
33
        /**
33
        /**
34
         * "Automated AJAX" plugin
34
         * "Automated AJAX" plugin
35
         */
35
         */
36
        const JWT_GENERATOR_AJAX   = 10;
36
        const JWT_GENERATOR_AJAX   = 10;
37
        /**
37
        /**
38
         * "REST API" plugin
38
         * "REST API" plugin
39
         */
39
         */
40
        const JWT_GENERATOR_REST   = 20;
40
        const JWT_GENERATOR_REST   = 20;
41
        /**
41
        /**
42
         * "Remember me" login method
42
         * "Remember me" login method
43
         */
43
         */
44
        const JWT_GENERATOR_LOGIN  = 40;
44
        const JWT_GENERATOR_LOGIN  = 40;
45
        /**
45
        /**
46
         * "Manually crafted" JWT tokens
46
         * "Manually crafted" JWT tokens
47
         */
47
         */
48
        const JWT_GENERATOR_MANUAL = 80;
48
        const JWT_GENERATOR_MANUAL = 80;
49
 
49
 
50
        /**
50
        /**
51
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
51
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
52
         * @param string $sub
52
         * @param string $sub
53
         * @return string
53
         * @return string
54
         */
54
         */
55
        private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
55
        private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
56
                // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long
56
                // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long
57
                return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
57
                return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
58
        }
58
        }
59
 
59
 
60
        /**
60
        /**
61
         * @param int $gen
61
         * @param int $gen
62
         */
62
         */
63
        private static function generatorName($gen) {
63
        private static function generatorName($gen) {
64
                // Note: The strings are not translated, because the name is used in config keys or logs
64
                // Note: The strings are not translated, because the name is used in config keys or logs
65
                if ($gen === self::JWT_GENERATOR_AJAX)   return 'Automated AJAX calls';
65
                if ($gen === self::JWT_GENERATOR_AJAX)   return 'Automated AJAX calls';
66
                if ($gen === self::JWT_GENERATOR_REST)   return 'REST API';
66
                if ($gen === self::JWT_GENERATOR_REST)   return 'REST API';
67
                if ($gen === self::JWT_GENERATOR_LOGIN)  return 'Login ("Remember me")';
67
                if ($gen === self::JWT_GENERATOR_LOGIN)  return 'Login ("Remember me")';
68
                if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created';
68
                if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created';
69
                return 'Unknown generator';
69
                return 'Unknown generator';
70
        }
70
        }
71
 
71
 
72
        /**
72
        /**
73
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
73
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
74
         * @param string $sub
74
         * @param string $sub
75
         * @return void
75
         * @return void
76
         * @throws OIDplusException
76
         * @throws OIDplusException
77
         */
77
         */
78
        public static function jwtBlacklist(int $gen, string $sub) {
78
        public static function jwtBlacklist(int $gen, string $sub) {
79
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
79
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
80
                $bl_time = time()-1;
80
                $bl_time = time()-1;
81
 
81
 
82
                $gen_desc = self::generatorName($gen);
82
                $gen_desc = self::generatorName($gen);
83
 
83
 
84
                OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
84
                OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', "$bl_time", OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
85
                OIDplus::config()->setValue($cfg, $bl_time);
85
                OIDplus::config()->setValue($cfg, $bl_time);
86
        }
86
        }
87
 
87
 
88
        /**
88
        /**
89
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
89
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
90
         * @param string $sub
90
         * @param string $sub
91
         * @return int
91
         * @return int
92
         * @throws OIDplusException
92
         * @throws OIDplusException
93
         */
93
         */
94
        public static function jwtGetBlacklistTime(int $gen, string $sub): int {
94
        public static function jwtGetBlacklistTime(int $gen, string $sub): int {
95
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
95
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
96
                return (int)OIDplus::config()->getValue($cfg,0);
96
                return (int)OIDplus::config()->getValue($cfg,0);
97
        }
97
        }
98
 
98
 
99
        /**
99
        /**
-
 
100
         * We include a hash of the server-secret here (ssh = server-secret-hash), so that the JWT can be invalidated by changing the server-secret
-
 
101
         * @return string
-
 
102
         * @throws OIDplusException
-
 
103
         */
-
 
104
        private static function getSsh(): string {
-
 
105
                return OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']);
-
 
106
        }
-
 
107
 
-
 
108
        /**
100
         * Do various checks if the token is allowed and not blacklisted
109
         * Do various checks if the token is allowed and not blacklisted
101
         * @param OIDplusAuthContentStore $contentProvider
110
         * @param OIDplusAuthContentStore $contentProvider
102
         * @param int|null $validGenerators Bitmask which generators to allow (null = allow all)
111
         * @param int|null $validGenerators Bitmask which generators to allow (null = allow all)
103
         * @return void
112
         * @return void
104
         * @throws OIDplusException
113
         * @throws OIDplusException
105
         */
114
         */
106
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=null) {
115
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=null) {
107
                // Check if the token is intended for us
116
                // Check if the token is intended for us
108
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
117
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
109
                        throw new OIDplusException(_L('Token has wrong audience'));
118
                        throw new OIDplusException(_L('Token has wrong audience'));
110
                }
119
                }
-
 
120
 
-
 
121
                if ($contentProvider->getValue('oidplus_ssh', '') !== self::getSsh()) {
-
 
122
                        throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore'));
-
 
123
                }
-
 
124
 
111
                $gen = $contentProvider->getValue('oidplus_generator', -1);
125
                $gen = $contentProvider->getValue('oidplus_generator', -1);
112
 
126
 
113
                $has_admin = $contentProvider->isAdminLoggedIn();
127
                $has_admin = $contentProvider->isAdminLoggedIn();
114
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
128
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
115
 
129
 
116
                // Check if the token generator is allowed
130
                // Check if the token generator is allowed
117
                if ($gen === self::JWT_GENERATOR_AJAX) {
131
                if ($gen === self::JWT_GENERATOR_AJAX) {
118
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
132
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
119
                                // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
133
                                // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
120
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
134
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
121
                        }
135
                        }
122
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
136
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
123
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
137
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
124
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
138
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
125
                        }
139
                        }
126
                }
140
                }
127
                else if ($gen === self::JWT_GENERATOR_REST) {
141
                else if ($gen === self::JWT_GENERATOR_REST) {
128
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
142
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
129
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
143
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
130
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN'));
144
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN'));
131
                        }
145
                        }
132
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
146
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
133
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
147
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
134
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER'));
148
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER'));
135
                        }
149
                        }
136
                }
150
                }
137
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
151
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
138
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
152
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
139
                        // - No PHP session will be used
153
                        // - No PHP session will be used
140
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
154
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
141
                        // - No server-side session needed
155
                        // - No server-side session needed
142
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
156
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
143
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
157
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
144
                        }
158
                        }
145
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
159
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
146
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
160
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
147
                        }
161
                        }
148
                }
162
                }
149
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
163
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
150
                        // Generator 2 are "hand-crafted" tokens
164
                        // Generator 2 are "hand-crafted" tokens
151
                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', false)) {
165
                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', false)) {
152
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
166
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
153
                        }
167
                        }
154
                } else {
168
                } else {
155
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
169
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
156
                }
170
                }
157
 
171
 
158
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
172
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
159
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
173
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
160
                // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well
174
                // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well
161
                // Small side effect: All "remember me" sessions of that user will be revoked then
175
                // Small side effect: All "remember me" sessions of that user will be revoked then
162
                $iat = $contentProvider->getValue('iat',0);
176
                $iat = $contentProvider->getValue('iat',0);
163
                if (($iat-120/*leeway 2min*/) > time()) {
177
                if (($iat-120/*leeway 2min*/) > time()) {
164
                        // Token was created in the future. Something is wrong!
178
                        // Token was created in the future. Something is wrong!
165
                        throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
179
                        throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
166
                }
180
                }
167
                $sublist = $contentProvider->loggedInRaList();
181
                $sublist = $contentProvider->loggedInRaList();
168
                $usernames = array();
182
                $usernames = array();
169
                foreach ($sublist as $sub) {
183
                foreach ($sublist as $sub) {
170
                        $usernames[] = $sub->raEmail();
184
                        $usernames[] = $sub->raEmail();
171
                }
185
                }
172
                if ($has_admin) $usernames[] = 'admin';
186
                if ($has_admin) $usernames[] = 'admin';
173
                foreach ($usernames as $username) {
187
                foreach ($usernames as $username) {
174
                        $bl_time = self::jwtGetBlacklistTime($gen, $username);
188
                        $bl_time = self::jwtGetBlacklistTime($gen, $username);
175
                        if ($iat <= $bl_time) {
189
                        if ($iat <= $bl_time) {
176
                                // Token is blacklisted (it was created before the last blacklist time)
190
                                // Token is blacklisted (it was created before the last blacklist time)
177
                                throw new OIDplusException(_L('The JWT token was blacklisted on %1. Please generate a new one',date('d F Y, H:i:s',$bl_time)));
191
                                throw new OIDplusException(_L('The JWT token was blacklisted on %1. Please generate a new one',date('d F Y, H:i:s',$bl_time)));
178
                        }
192
                        }
179
                }
193
                }
180
 
194
 
181
                // Optional feature: Limit the JWT to a specific IP address
195
                // Optional feature: Limit the JWT to a specific IP address
182
                // Currently not used in OIDplus
196
                // Currently not used in OIDplus
183
                $ip = $contentProvider->getValue('ip','');
197
                $ip = $contentProvider->getValue('ip','');
184
                if ($ip !== '') {
198
                if ($ip !== '') {
185
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
199
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
186
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
200
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
187
                        }
201
                        }
188
                }
202
                }
189
 
203
 
190
                // Checks if JWT are dependent on the generator
204
                // Checks if JWT are dependent on the generator
191
                if (!is_null($validGenerators)) {
205
                if (!is_null($validGenerators)) {
192
                        if (($gen & $validGenerators) === 0) {
206
                        if (($gen & $validGenerators) === 0) {
193
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
207
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
194
                        }
208
                        }
195
                }
209
                }
196
        }
210
        }
197
 
211
 
198
        // Override abstract functions
212
        // Override abstract functions
199
 
213
 
200
        /**
214
        /**
201
         * @return void
215
         * @return void
202
         */
216
         */
203
        public function activate() {
217
        public function activate() {
204
                // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
218
                // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
205
                OIDplus::register_shutdown_function(array($this,'activateNow'));
219
                OIDplus::register_shutdown_function(array($this,'activateNow'));
206
        }
220
        }
207
 
221
 
208
        /**
222
        /**
209
         * @return void
223
         * @return void
210
         * @throws OIDplusException
224
         * @throws OIDplusException
211
         */
225
         */
212
        public function activateNow() {
226
        public function activateNow() {
213
                $token = $this->getJWTToken();
227
                $token = $this->getJWTToken();
214
                $exp = $this->getValue('exp',0);
228
                $exp = $this->getValue('exp',0);
215
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
229
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
216
        }
230
        }
217
 
231
 
218
        /**
232
        /**
219
         * @return void
233
         * @return void
220
         * @throws OIDplusException
234
         * @throws OIDplusException
221
         */
235
         */
222
        public function destroySession() {
236
        public function destroySession() {
223
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
237
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
224
        }
238
        }
225
 
239
 
226
        /**
240
        /**
227
         * @param string $email
241
         * @param string $email
228
         * @return void
242
         * @return void
229
         * @throws OIDplusException
243
         * @throws OIDplusException
230
         */
244
         */
231
        public function raLogout(string $email) {
245
        public function raLogout(string $email) {
232
                $gen = $this->getValue('oidplus_generator', -1);
246
                $gen = $this->getValue('oidplus_generator', -1);
233
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
247
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
234
                parent::raLogout($email);
248
                parent::raLogout($email);
235
        }
249
        }
236
 
250
 
237
        /**
251
        /**
238
         * @param string $email
252
         * @param string $email
239
         * @param string $loginfo
253
         * @param string $loginfo
240
         * @return void
254
         * @return void
241
         * @throws OIDplusException
255
         * @throws OIDplusException
242
         */
256
         */
243
        public function raLogoutEx(string $email, string &$loginfo) {
257
        public function raLogoutEx(string $email, string &$loginfo) {
244
                $this->raLogout($email);
258
                $this->raLogout($email);
245
                $loginfo = 'from JWT session';
259
                $loginfo = 'from JWT session';
246
        }
260
        }
247
 
261
 
248
        /**
262
        /**
249
         * @return void
263
         * @return void
250
         * @throws OIDplusException
264
         * @throws OIDplusException
251
         */
265
         */
252
        public function adminLogout() {
266
        public function adminLogout() {
253
                $gen = $this->getValue('oidplus_generator', -1);
267
                $gen = $this->getValue('oidplus_generator', -1);
254
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
268
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
255
                parent::adminLogout();
269
                parent::adminLogout();
256
        }
270
        }
257
 
271
 
258
        /**
272
        /**
259
         * @param string $loginfo
273
         * @param string $loginfo
260
         * @return void
274
         * @return void
261
         * @throws OIDplusException
275
         * @throws OIDplusException
262
         */
276
         */
263
        public function adminLogoutEx(string &$loginfo) {
277
        public function adminLogoutEx(string &$loginfo) {
264
                $this->adminLogout();
278
                $this->adminLogout();
265
                $loginfo = 'from JWT session';
279
                $loginfo = 'from JWT session';
266
        }
280
        }
267
 
281
 
268
        private static $contentProvider = null;
282
        private static $contentProvider = null;
269
 
283
 
270
        /**
284
        /**
271
         * @return OIDplusAuthContentStore|null
285
         * @return OIDplusAuthContentStore|null
272
         * @throws OIDplusException
286
         * @throws OIDplusException
273
         */
287
         */
274
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
288
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
275
                if (!self::$contentProvider) {
289
                if (!self::$contentProvider) {
276
 
290
 
277
                        $tmp = null;
291
                        $tmp = null;
278
                        $silent_error = false;
292
                        $silent_error = false;
279
 
293
 
280
                        try {
294
                        try {
281
 
295
 
282
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
296
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
283
                                if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core.
297
                                if (str_starts_with($rel_url, 'rest/')) { // <== TODO: Find a way how to move this into the plugin, since REST does not belong to the core.
284
 
298
 
285
                                        // REST may only use Bearer Authentication
299
                                        // REST may only use Bearer Authentication
286
                                        $bearer = getBearerToken();
300
                                        $bearer = getBearerToken();
287
                                        if (!is_null($bearer)) {
301
                                        if (!is_null($bearer)) {
288
                                                $silent_error = false;
302
                                                $silent_error = false;
289
                                                $tmp = new OIDplusAuthContentStoreJWT();
303
                                                $tmp = new OIDplusAuthContentStoreJWT();
290
                                                $tmp->loadJWT($bearer);
304
                                                $tmp->loadJWT($bearer);
291
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
305
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
292
                                        }
306
                                        }
293
 
307
 
294
                                } else {
308
                                } else {
295
 
309
 
296
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie
310
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie
297
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
311
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
298
                                                $silent_error = true;
312
                                                $silent_error = true;
299
                                                $tmp = new OIDplusAuthContentStoreJWT();
313
                                                $tmp = new OIDplusAuthContentStoreJWT();
300
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
314
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
301
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
315
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
302
                                        }
316
                                        }
303
 
317
 
304
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie)
318
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie)
305
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
319
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
306
                                                if (isset($_POST[self::COOKIE_NAME])) {
320
                                                if (isset($_POST[self::COOKIE_NAME])) {
307
                                                        $silent_error = false;
321
                                                        $silent_error = false;
308
                                                        $tmp = new OIDplusAuthContentStoreJWT();
322
                                                        $tmp = new OIDplusAuthContentStoreJWT();
309
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
323
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
310
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
324
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
311
                                                }
325
                                                }
312
                                                if (isset($_GET[self::COOKIE_NAME])) {
326
                                                if (isset($_GET[self::COOKIE_NAME])) {
313
                                                        $silent_error = false;
327
                                                        $silent_error = false;
314
                                                        $tmp = new OIDplusAuthContentStoreJWT();
328
                                                        $tmp = new OIDplusAuthContentStoreJWT();
315
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
329
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
316
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
330
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
317
                                                }
331
                                                }
318
                                        }
332
                                        }
319
 
333
 
320
                                }
334
                                }
321
 
335
 
322
                        } catch (\Exception $e) {
336
                        } catch (\Exception $e) {
323
                                if (!$silent_error) {
337
                                if (!$silent_error) {
324
                                        // Most likely an AJAX request. We can throw an Exception
338
                                        // Most likely an AJAX request. We can throw an Exception
325
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
339
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
326
                                } else {
340
                                } else {
327
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
341
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
328
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
342
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
329
                                        return null;
343
                                        return null;
330
                                }
344
                                }
331
                        }
345
                        }
332
 
346
 
333
                        self::$contentProvider = $tmp;
347
                        self::$contentProvider = $tmp;
334
                }
348
                }
335
 
349
 
336
                return self::$contentProvider;
350
                return self::$contentProvider;
337
        }
351
        }
338
 
352
 
339
        /**
353
        /**
340
         * @param string $email
354
         * @param string $email
341
         * @param string $loginfo
355
         * @param string $loginfo
342
         * @return void
356
         * @return void
343
         * @throws OIDplusException
357
         * @throws OIDplusException
344
         */
358
         */
345
        public function raLoginEx(string $email, string &$loginfo) {
359
        public function raLoginEx(string $email, string &$loginfo) {
346
                if (is_null(self::getActiveProvider())) {
360
                if (is_null(self::getActiveProvider())) {
347
                        $this->raLogin($email);
361
                        $this->raLogin($email);
348
                        $loginfo = 'into new JWT session';
362
                        $loginfo = 'into new JWT session';
349
                        self::$contentProvider = $this;
363
                        self::$contentProvider = $this;
350
                } else {
364
                } else {
351
                        $gen = $this->getValue('oidplus_generator',-1);
365
                        $gen = $this->getValue('oidplus_generator',-1);
352
                        switch ($gen) {
366
                        switch ($gen) {
353
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
367
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
354
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
368
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
355
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
369
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
356
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
370
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
357
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
371
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
358
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
372
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
359
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
373
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
360
                                        }
374
                                        }
361
                                        break;
375
                                        break;
362
                                default:
376
                                default:
363
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
377
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
364
                                        break;
378
                                        break;
365
                        }
379
                        }
366
                        $this->raLogin($email);
380
                        $this->raLogin($email);
367
                        $loginfo = 'into existing JWT session';
381
                        $loginfo = 'into existing JWT session';
368
                }
382
                }
369
        }
383
        }
370
 
384
 
371
        /**
385
        /**
372
         * @param string $loginfo
386
         * @param string $loginfo
373
         * @return void
387
         * @return void
374
         * @throws OIDplusException
388
         * @throws OIDplusException
375
         */
389
         */
376
        public function adminLoginEx(string &$loginfo) {
390
        public function adminLoginEx(string &$loginfo) {
377
                if (is_null(self::getActiveProvider())) {
391
                if (is_null(self::getActiveProvider())) {
378
                        $this->adminLogin();
392
                        $this->adminLogin();
379
                        $loginfo = 'into new JWT session';
393
                        $loginfo = 'into new JWT session';
380
                        self::$contentProvider = $this;
394
                        self::$contentProvider = $this;
381
                } else {
395
                } else {
382
                        $gen = $this->getValue('oidplus_generator',-1);
396
                        $gen = $this->getValue('oidplus_generator',-1);
383
                        switch ($gen) {
397
                        switch ($gen) {
384
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
398
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
385
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
399
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
386
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
400
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
387
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
401
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
388
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
402
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
389
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
403
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
390
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
404
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
391
                                        }
405
                                        }
392
                                        break;
406
                                        break;
393
                                default:
407
                                default:
394
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
408
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
395
                                        break;
409
                                        break;
396
                        }
410
                        }
397
                        $this->adminLogin();
411
                        $this->adminLogin();
398
                        $loginfo = 'into existing JWT session';
412
                        $loginfo = 'into existing JWT session';
399
                }
413
                }
400
        }
414
        }
401
 
415
 
402
        // Individual functions
416
        // Individual functions
403
 
417
 
404
        /**
418
        /**
405
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
419
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
406
         * @param string $jwt
420
         * @param string $jwt
407
         * @return void
421
         * @return void
408
         * @throws OIDplusException
422
         * @throws OIDplusException
409
         */
423
         */
410
        public function loadJWT(string $jwt) {
424
        public function loadJWT(string $jwt) {
411
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
425
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
412
                if (OIDplus::getPkiStatus()) {
426
                if (OIDplus::getPkiStatus()) {
413
                        $pubKey = OIDplus::getSystemPublicKey();
427
                        $pubKey = OIDplus::getSystemPublicKey();
414
                        $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 ist hardcoded in getPkiStatus() generation
428
                        $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
415
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
429
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
416
                } else {
430
                } else {
417
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
431
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
418
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
432
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
419
                        $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
433
                        $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
420
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
434
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
421
                }
435
                }
422
        }
436
        }
423
 
437
 
424
        /**
438
        /**
425
         * @return string
439
         * @return string
426
         * @throws OIDplusException
440
         * @throws OIDplusException
427
         */
441
         */
428
        public function getJWTToken(): string {
442
        public function getJWTToken(): string {
429
                $payload = $this->content;
443
                $payload = $this->content;
430
                $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
444
                $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
431
                $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
445
                $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
432
                $payload["jti"] = gen_uuid();
446
                $payload["jti"] = gen_uuid();
433
                $payload["iat"] = time();
447
                $payload["iat"] = time();
-
 
448
                $payload["oidplus_ssh"] = self::getSsh(); // SSH = Server Secret Hash
434
 
449
 
435
                if (OIDplus::getPkiStatus()) {
450
                if (OIDplus::getPkiStatus()) {
436
                        $privKey = OIDplus::getSystemPrivateKey();
451
                        $privKey = OIDplus::getSystemPrivateKey();
437
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 ist hardcoded in getPkiStatus() generation
452
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
438
                } else {
453
                } else {
439
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
454
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
440
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
455
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
441
                        return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
456
                        return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
442
                }
457
                }
443
        }
458
        }
444
 
459
 
445
}
460
}
446
 
461