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 |