Subversion Repositories oidplus

Rev

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

Rev 1300 Rev 1301
1
<?php
1
<?php
2
 
2
 
3
/*
3
/*
4
 * OIDplus 2.0
4
 * OIDplus 2.0
5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
6
 *
6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
9
 * You may obtain a copy of the License at
10
 *
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
17
 * limitations under the License.
18
 */
18
 */
19
 
19
 
20
namespace ViaThinkSoft\OIDplus;
20
namespace ViaThinkSoft\OIDplus;
21
 
21
 
22
// phpcs:disable PSR1.Files.SideEffects
22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
24
// phpcs:enable PSR1.Files.SideEffects
25
 
25
 
26
class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy {
26
class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStore {
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
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
101
         * @return string
102
         * @throws OIDplusException
102
         * @throws OIDplusException
103
         */
103
         */
104
        private static function getSsh(): string {
104
        private static function getSsh(): string {
105
                return OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']);
105
                return OIDplus::authUtils()->makeSecret(['bb1aebd6-fe6a-11ed-a553-3c4a92df8582']);
106
        }
106
        }
107
 
107
 
108
        /**
108
        /**
109
         * Do various checks if the token is allowed and not blacklisted
109
         * Do various checks if the token is allowed and not blacklisted
110
         * @param OIDplusAuthContentStore $contentProvider
110
         * @param OIDplusAuthContentStore $contentProvider
111
         * @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)
112
         * @return void
112
         * @return void
113
         * @throws OIDplusException
113
         * @throws OIDplusException
114
         */
114
         */
115
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=null) {
115
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=null) {
116
                // Check if the token is intended for us
116
                // Check if the token is intended for us
117
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
117
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
118
                        throw new OIDplusException(_L('Token has wrong audience'));
118
                        throw new OIDplusException(_L('Token has wrong audience'));
119
                }
119
                }
120
 
120
 
121
                if ($contentProvider->getValue('oidplus_ssh', '') !== self::getSsh()) {
121
                if ($contentProvider->getValue('oidplus_ssh', '') !== self::getSsh()) {
122
                        throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore'));
122
                        throw new OIDplusException(_L('"Server Secret" was changed; therefore the JWT is not valid anymore'));
123
                }
123
                }
124
 
124
 
125
                $gen = $contentProvider->getValue('oidplus_generator', -1);
125
                $gen = $contentProvider->getValue('oidplus_generator', -1);
126
 
126
 
127
                $has_admin = $contentProvider->isAdminLoggedIn();
127
                $has_admin = $contentProvider->isAdminLoggedIn();
128
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
128
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
129
 
129
 
130
                // Check if the token generator is allowed
130
                // Check if the token generator is allowed
131
                if ($gen === self::JWT_GENERATOR_AJAX) {
131
                if ($gen === self::JWT_GENERATOR_AJAX) {
132
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
132
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
133
                                // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
133
                                // Generator: plugins/viathinksoft/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
134
                                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'));
135
                        }
135
                        }
136
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
136
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
137
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
137
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
138
                                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'));
139
                        }
139
                        }
140
                }
140
                }
141
                else if ($gen === self::JWT_GENERATOR_REST) {
141
                else if ($gen === self::JWT_GENERATOR_REST) {
142
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
142
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
143
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
143
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
144
                                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'));
145
                        }
145
                        }
146
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
146
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
147
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
147
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
148
                                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'));
149
                        }
149
                        }
150
                }
150
                }
151
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
151
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
152
                        // 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):
153
                        // - No PHP session will be used
153
                        // - No PHP session will be used
154
                        // - 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)
155
                        // - No server-side session needed
155
                        // - No server-side session needed
156
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
156
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
157
                                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'));
158
                        }
158
                        }
159
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
159
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
160
                                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'));
161
                        }
161
                        }
162
                }
162
                }
163
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
163
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
164
                        // Generator: "hand-crafted" tokens
164
                        // Generator: "hand-crafted" tokens
165
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_ADMIN', false)) {
165
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_ADMIN', false)) {
166
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_ADMIN'));
166
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_ADMIN'));
167
                        }
167
                        }
168
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_USER', false)) {
168
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL_USER', false)) {
169
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_USER'));
169
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL_USER'));
170
                        }
170
                        }
171
                } else {
171
                } else {
172
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
172
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
173
                }
173
                }
174
 
174
 
175
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
175
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
176
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
176
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
177
                // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well
177
                // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well
178
                // Small side effect: All "remember me" sessions of that user will be revoked then
178
                // Small side effect: All "remember me" sessions of that user will be revoked then
179
                $iat = $contentProvider->getValue('iat',0);
179
                $iat = $contentProvider->getValue('iat',0);
180
                if (($iat-120/*leeway 2min*/) > time()) {
180
                if (($iat-120/*leeway 2min*/) > time()) {
181
                        // Token was created in the future. Something is wrong!
181
                        // Token was created in the future. Something is wrong!
182
                        throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
182
                        throw new OIDplusException(_L('JWT Token cannot be verified because the server time is wrong'));
183
                }
183
                }
184
                $sublist = $contentProvider->loggedInRaList();
184
                $sublist = $contentProvider->loggedInRaList();
185
                $usernames = array();
185
                $usernames = array();
186
                foreach ($sublist as $sub) {
186
                foreach ($sublist as $sub) {
187
                        $usernames[] = $sub->raEmail();
187
                        $usernames[] = $sub->raEmail();
188
                }
188
                }
189
                if ($has_admin) $usernames[] = 'admin';
189
                if ($has_admin) $usernames[] = 'admin';
190
                foreach ($usernames as $username) {
190
                foreach ($usernames as $username) {
191
                        $bl_time = self::jwtGetBlacklistTime($gen, $username);
191
                        $bl_time = self::jwtGetBlacklistTime($gen, $username);
192
                        if ($iat <= $bl_time) {
192
                        if ($iat <= $bl_time) {
193
                                // Token is blacklisted (it was created before the last blacklist time)
193
                                // Token is blacklisted (it was created before the last blacklist time)
194
                                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)));
194
                                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)));
195
                        }
195
                        }
196
                }
196
                }
197
 
197
 
198
                // Optional feature: Limit the JWT to a specific IP address
198
                // Optional feature: Limit the JWT to a specific IP address
199
                // Currently not used in OIDplus
199
                // Currently not used in OIDplus
200
                $ip = $contentProvider->getValue('oidplus_limit_ip','');
200
                $ip = $contentProvider->getValue('oidplus_limit_ip','');
201
                if ($ip !== '') {
201
                if ($ip !== '') {
202
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
202
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
203
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
203
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
204
                        }
204
                        }
205
                }
205
                }
206
 
206
 
207
                // Checks if JWT are dependent on the generator
207
                // Checks if JWT are dependent on the generator
208
                if (!is_null($validGenerators)) {
208
                if (!is_null($validGenerators)) {
209
                        if (($gen & $validGenerators) === 0) {
209
                        if (($gen & $validGenerators) === 0) {
210
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
210
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
211
                        }
211
                        }
212
                }
212
                }
213
        }
213
        }
214
 
214
 
215
        // Override abstract functions
215
        // Override abstract functions
216
 
216
 
-
 
217
        /**
-
 
218
         * @var array
-
 
219
         */
-
 
220
        protected $content = array();
-
 
221
 
-
 
222
        /**
-
 
223
         * @param string $name
-
 
224
         * @param mixed|null $default
-
 
225
         * @return mixed|null
-
 
226
         */
-
 
227
        public function getValue(string $name, $default = NULL) {
-
 
228
                return $this->content[$name] ?? $default;
-
 
229
        }
-
 
230
 
-
 
231
        /**
-
 
232
         * @param string $name
-
 
233
         * @param mixed $value
-
 
234
         * @return void
-
 
235
         */
-
 
236
        public function setValue(string $name, $value) {
-
 
237
                $this->content[$name] = $value;
-
 
238
        }
-
 
239
 
-
 
240
        /**
-
 
241
         * @param string $name
-
 
242
         * @return bool
-
 
243
         */
-
 
244
        public function exists(string $name): bool {
-
 
245
                return isset($this->content[$name]);
-
 
246
        }
-
 
247
 
-
 
248
        /**
-
 
249
         * @param string $name
-
 
250
         * @return void
-
 
251
         */
-
 
252
        public function delete(string $name) {
-
 
253
                unset($this->content[$name]);
-
 
254
        }
-
 
255
 
217
        /**
256
        /**
218
         * @return void
257
         * @return void
219
         */
258
         */
220
        public function activate() {
259
        public function activate() {
221
                // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
260
                // Send cookie at the end of the HTTP request, in case there are multiple activate() calls
222
                OIDplus::register_shutdown_function(array($this,'activateNow'));
261
                OIDplus::register_shutdown_function(array($this,'activateNow'));
223
        }
262
        }
224
 
263
 
225
        /**
264
        /**
226
         * @return void
265
         * @return void
227
         * @throws OIDplusException
266
         * @throws OIDplusException
228
         */
267
         */
229
        public function activateNow() {
268
        public function activateNow() {
230
                $token = $this->getJWTToken();
269
                $token = $this->getJWTToken();
231
                $exp = $this->getValue('exp',0);
270
                $exp = $this->getValue('exp',0);
232
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
271
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
233
        }
272
        }
234
 
273
 
235
        /**
274
        /**
236
         * @return void
275
         * @return void
237
         * @throws OIDplusException
276
         * @throws OIDplusException
238
         */
277
         */
239
        public function destroySession() {
278
        public function destroySession() {
240
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
279
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
241
        }
280
        }
242
 
281
 
243
        /**
282
        /**
244
         * @param string $email
283
         * @param string $email
245
         * @return void
284
         * @return void
246
         * @throws OIDplusException
285
         * @throws OIDplusException
247
         */
286
         */
248
        public function raLogout(string $email) {
287
        public function raLogout(string $email) {
249
                $gen = $this->getValue('oidplus_generator', -1);
288
                $gen = $this->getValue('oidplus_generator', -1);
250
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
289
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
251
                parent::raLogout($email);
290
                parent::raLogout($email);
252
        }
291
        }
253
 
292
 
254
        /**
293
        /**
255
         * @param string $email
294
         * @param string $email
256
         * @param string $loginfo
295
         * @param string $loginfo
257
         * @return void
296
         * @return void
258
         * @throws OIDplusException
297
         * @throws OIDplusException
259
         */
298
         */
260
        public function raLogoutEx(string $email, string &$loginfo) {
299
        public function raLogoutEx(string $email, string &$loginfo) {
261
                $this->raLogout($email);
300
                $this->raLogout($email);
262
                $loginfo = 'from JWT session';
301
                $loginfo = 'from JWT session';
263
        }
302
        }
264
 
303
 
265
        /**
304
        /**
266
         * @return void
305
         * @return void
267
         * @throws OIDplusException
306
         * @throws OIDplusException
268
         */
307
         */
269
        public function adminLogout() {
308
        public function adminLogout() {
270
                $gen = $this->getValue('oidplus_generator', -1);
309
                $gen = $this->getValue('oidplus_generator', -1);
271
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
310
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
272
                parent::adminLogout();
311
                parent::adminLogout();
273
        }
312
        }
274
 
313
 
275
        /**
314
        /**
276
         * @param string $loginfo
315
         * @param string $loginfo
277
         * @return void
316
         * @return void
278
         * @throws OIDplusException
317
         * @throws OIDplusException
279
         */
318
         */
280
        public function adminLogoutEx(string &$loginfo) {
319
        public function adminLogoutEx(string &$loginfo) {
281
                $this->adminLogout();
320
                $this->adminLogout();
282
                $loginfo = 'from JWT session';
321
                $loginfo = 'from JWT session';
283
        }
322
        }
284
 
323
 
285
        private static $contentProvider = null;
324
        private static $contentProvider = null;
286
 
325
 
287
        /**
326
        /**
288
         * @return OIDplusAuthContentStore|null
327
         * @return OIDplusAuthContentStore|null
289
         * @throws OIDplusException
328
         * @throws OIDplusException
290
         */
329
         */
291
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
330
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
292
                if (!self::$contentProvider) {
331
                if (!self::$contentProvider) {
293
 
332
 
294
                        $tmp = null;
333
                        $tmp = null;
295
                        $silent_error = false;
334
                        $silent_error = false;
296
 
335
 
297
                        try {
336
                        try {
298
 
337
 
299
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
338
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
300
                                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.
339
                                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.
301
 
340
 
302
                                        // REST may only use Bearer Authentication
341
                                        // REST may only use Bearer Authentication
303
                                        $bearer = getBearerToken();
342
                                        $bearer = getBearerToken();
304
                                        if (!is_null($bearer)) {
343
                                        if (!is_null($bearer)) {
305
                                                $silent_error = false;
344
                                                $silent_error = false;
306
                                                $tmp = new OIDplusAuthContentStoreJWT();
345
                                                $tmp = new OIDplusAuthContentStoreJWT();
307
                                                $tmp->loadJWT($bearer);
346
                                                $tmp->loadJWT($bearer);
308
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
347
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
309
                                        }
348
                                        }
310
 
349
 
311
                                } else {
350
                                } else {
312
 
351
 
313
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie
352
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie
314
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
353
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
315
                                                $silent_error = true;
354
                                                $silent_error = true;
316
                                                $tmp = new OIDplusAuthContentStoreJWT();
355
                                                $tmp = new OIDplusAuthContentStoreJWT();
317
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
356
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
318
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
357
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
319
                                        }
358
                                        }
320
 
359
 
321
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie)
360
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie)
322
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
361
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
323
                                                if (isset($_POST[self::COOKIE_NAME])) {
362
                                                if (isset($_POST[self::COOKIE_NAME])) {
324
                                                        $silent_error = false;
363
                                                        $silent_error = false;
325
                                                        $tmp = new OIDplusAuthContentStoreJWT();
364
                                                        $tmp = new OIDplusAuthContentStoreJWT();
326
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
365
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
327
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
366
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
328
                                                }
367
                                                }
329
                                                if (isset($_GET[self::COOKIE_NAME])) {
368
                                                if (isset($_GET[self::COOKIE_NAME])) {
330
                                                        $silent_error = false;
369
                                                        $silent_error = false;
331
                                                        $tmp = new OIDplusAuthContentStoreJWT();
370
                                                        $tmp = new OIDplusAuthContentStoreJWT();
332
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
371
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
333
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
372
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
334
                                                }
373
                                                }
335
                                        }
374
                                        }
336
 
375
 
337
                                }
376
                                }
338
 
377
 
339
                        } catch (\Exception $e) {
378
                        } catch (\Exception $e) {
340
                                if (!$silent_error) {
379
                                if (!$silent_error) {
341
                                        // Most likely an AJAX request. We can throw an Exception
380
                                        // Most likely an AJAX request. We can throw an Exception
342
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
381
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
343
                                } else {
382
                                } else {
344
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
383
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
345
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
384
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
346
                                        return null;
385
                                        return null;
347
                                }
386
                                }
348
                        }
387
                        }
349
 
388
 
350
                        self::$contentProvider = $tmp;
389
                        self::$contentProvider = $tmp;
351
                }
390
                }
352
 
391
 
353
                return self::$contentProvider;
392
                return self::$contentProvider;
354
        }
393
        }
355
 
394
 
356
        /**
395
        /**
357
         * @param string $email
396
         * @param string $email
358
         * @param string $loginfo
397
         * @param string $loginfo
359
         * @return void
398
         * @return void
360
         * @throws OIDplusException
399
         * @throws OIDplusException
361
         */
400
         */
362
        public function raLoginEx(string $email, string &$loginfo) {
401
        public function raLoginEx(string $email, string &$loginfo) {
363
                if (is_null(self::getActiveProvider())) {
402
                if (is_null(self::getActiveProvider())) {
364
                        $this->raLogin($email);
403
                        $this->raLogin($email);
365
                        $loginfo = 'into new JWT session';
404
                        $loginfo = 'into new JWT session';
366
                        self::$contentProvider = $this;
405
                        self::$contentProvider = $this;
367
                } else {
406
                } else {
368
                        $gen = $this->getValue('oidplus_generator',-1);
407
                        $gen = $this->getValue('oidplus_generator',-1);
369
                        switch ($gen) {
408
                        switch ($gen) {
370
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
409
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
371
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
410
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
372
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
411
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
373
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
412
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
374
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
413
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
375
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
414
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
376
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
415
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
377
                                        }
416
                                        }
378
                                        break;
417
                                        break;
379
                                default:
418
                                default:
380
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
419
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
381
                                        break;
420
                                        break;
382
                        }
421
                        }
383
                        $this->raLogin($email);
422
                        $this->raLogin($email);
384
                        $loginfo = 'into existing JWT session';
423
                        $loginfo = 'into existing JWT session';
385
                }
424
                }
386
        }
425
        }
387
 
426
 
388
        /**
427
        /**
389
         * @param string $loginfo
428
         * @param string $loginfo
390
         * @return void
429
         * @return void
391
         * @throws OIDplusException
430
         * @throws OIDplusException
392
         */
431
         */
393
        public function adminLoginEx(string &$loginfo) {
432
        public function adminLoginEx(string &$loginfo) {
394
                if (is_null(self::getActiveProvider())) {
433
                if (is_null(self::getActiveProvider())) {
395
                        $this->adminLogin();
434
                        $this->adminLogin();
396
                        $loginfo = 'into new JWT session';
435
                        $loginfo = 'into new JWT session';
397
                        self::$contentProvider = $this;
436
                        self::$contentProvider = $this;
398
                } else {
437
                } else {
399
                        $gen = $this->getValue('oidplus_generator',-1);
438
                        $gen = $this->getValue('oidplus_generator',-1);
400
                        switch ($gen) {
439
                        switch ($gen) {
401
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
440
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
402
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
441
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
403
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
442
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
404
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
443
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
405
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
444
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
406
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
445
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
407
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
446
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
408
                                        }
447
                                        }
409
                                        break;
448
                                        break;
410
                                default:
449
                                default:
411
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
450
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
412
                                        break;
451
                                        break;
413
                        }
452
                        }
414
                        $this->adminLogin();
453
                        $this->adminLogin();
415
                        $loginfo = 'into existing JWT session';
454
                        $loginfo = 'into existing JWT session';
416
                }
455
                }
417
        }
456
        }
418
 
457
 
419
        // Individual functions
458
        // Individual functions
420
 
459
 
421
        /**
460
        /**
422
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
461
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
423
         * @param string $jwt
462
         * @param string $jwt
424
         * @return void
463
         * @return void
425
         * @throws OIDplusException
464
         * @throws OIDplusException
426
         */
465
         */
427
        public function loadJWT(string $jwt) {
466
        public function loadJWT(string $jwt) {
428
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
467
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
429
                if (OIDplus::getPkiStatus()) {
468
                if (OIDplus::getPkiStatus()) {
430
                        $pubKey = OIDplus::getSystemPublicKey();
469
                        $pubKey = OIDplus::getSystemPublicKey();
431
                        $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
470
                        $k = new \Firebase\JWT\Key($pubKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
432
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
471
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
433
                } else {
472
                } else {
434
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
473
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
435
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
474
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
436
                        $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
475
                        $k = new \Firebase\JWT\Key($key, 'HS512'); // HMAC+SHA512 is hardcoded here
437
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
476
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $k);
438
                }
477
                }
439
        }
478
        }
440
 
479
 
441
        /**
480
        /**
442
         * @return string
481
         * @return string
443
         * @throws OIDplusException
482
         * @throws OIDplusException
444
         */
483
         */
445
        public function getJWTToken(): string {
484
        public function getJWTToken(): string {
446
                $payload = $this->content;
485
                $payload = $this->content;
447
                $payload["oidplus_ssh"] = self::getSsh(); // SSH = Server Secret Hash
486
                $payload["oidplus_ssh"] = self::getSsh(); // SSH = Server Secret Hash
448
                $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
487
                $payload["iss"] = OIDplus::getEditionInfo()['jwtaud'];
449
                $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
488
                $payload["aud"] = OIDplus::getEditionInfo()['jwtaud'];
450
                $payload["jti"] = gen_uuid();
489
                $payload["jti"] = gen_uuid();
451
                $payload["iat"] = time();
490
                $payload["iat"] = time();
452
 
491
 
453
                if (OIDplus::getPkiStatus()) {
492
                if (OIDplus::getPkiStatus()) {
454
                        $privKey = OIDplus::getSystemPrivateKey();
493
                        $privKey = OIDplus::getSystemPrivateKey();
455
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
494
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS256'); // RSA+SHA256 is hardcoded in getPkiStatus() generation
456
                } else {
495
                } else {
457
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
496
                        $key = OIDplus::authUtils()->makeSecret(['0be35e52-f4ef-11ed-b67e-3c4a92df8582']);
458
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
497
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 32/*256bit*/, false);
459
                        return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
498
                        return \Firebase\JWT\JWT::encode($payload, $key, 'HS512'); // HMAC+SHA512 is hardcoded here
460
                }
499
                }
461
        }
500
        }
462
 
501
 
463
}
502
}
464
 
503