Subversion Repositories oidplus

Rev

Rev 1130 | Rev 1277 | Go to most recent revision | Show entire file | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 1130 Rev 1265
Line 31... Line 31...
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   = 0;
36
        const JWT_GENERATOR_AJAX   = 10;
-
 
37
        /**
-
 
38
         * "REST API" plugin
-
 
39
         */
-
 
40
        const JWT_GENERATOR_REST   = 20;
37
        /**
41
        /**
38
         * "Remember me" login method
42
         * "Remember me" login method
39
         */
43
         */
40
        const JWT_GENERATOR_LOGIN  = 1;
44
        const JWT_GENERATOR_LOGIN  = 40;
41
        /**
45
        /**
42
         * "Manually crafted" JWT tokens
46
         * "Manually crafted" JWT tokens
43
         */
47
         */
44
        const JWT_GENERATOR_MANUAL = 2;
48
        const JWT_GENERATOR_MANUAL = 80;
45
 
49
 
46
        /**
50
        /**
47
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
51
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
48
         * @param string $sub
52
         * @param string $sub
49
         * @return string
53
         * @return string
50
         */
54
         */
51
        private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
55
        private static function jwtGetBlacklistConfigKey(int $gen, string $sub): string {
52
                // Note: Needs to be <= 50 characters!
56
                // Note: Needs to be <= 50 characters! If $gen is 2 chars, then the config key is 49 chars long
53
                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)),'=').')';
54
        }
58
        }
55
 
59
 
56
        /**
60
        /**
-
 
61
         * @param int $gen
-
 
62
         */
-
 
63
        private static function generatorName($gen) {
-
 
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';
-
 
66
                if ($gen === self::JWT_GENERATOR_REST)   return 'REST API';
-
 
67
                if ($gen === self::JWT_GENERATOR_LOGIN)  return 'Login ("Remember me")';
-
 
68
                if ($gen === self::JWT_GENERATOR_MANUAL) return 'Manually created';
-
 
69
                return 'Unknown generator';
-
 
70
        }
-
 
71
 
-
 
72
        /**
57
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
73
         * @param int $gen OIDplusAuthContentStoreJWT::JWT_GENERATOR_...
58
         * @param string $sub
74
         * @param string $sub
59
         * @return void
75
         * @return void
60
         * @throws OIDplusException
76
         * @throws OIDplusException
61
         */
77
         */
62
        public static function jwtBlacklist(int $gen, string $sub) {
78
        public static function jwtBlacklist(int $gen, string $sub) {
63
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
79
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
64
                $bl_time = time()-1;
80
                $bl_time = time()-1;
65
 
81
 
66
                $gen_desc = 'Unknown';
82
                $gen_desc = self::generatorName($gen);
67
                if ($gen === self::JWT_GENERATOR_AJAX)   $gen_desc = 'Automated AJAX calls';
-
 
68
                if ($gen === self::JWT_GENERATOR_LOGIN)  $gen_desc = 'Login ("Remember me")';
-
 
69
                if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created';
-
 
70
 
83
 
71
                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) {});
72
                OIDplus::config()->setValue($cfg, $bl_time);
85
                OIDplus::config()->setValue($cfg, $bl_time);
73
        }
86
        }
74
 
87
 
Line 82... Line 95...
82
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
95
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
83
                return (int)OIDplus::config()->getValue($cfg,0);
96
                return (int)OIDplus::config()->getValue($cfg,0);
84
        }
97
        }
85
 
98
 
86
        /**
99
        /**
-
 
100
         * Do various checks if the token is allowed and not blacklisted
87
         * @param OIDplusAuthContentStore $contentProvider
101
         * @param OIDplusAuthContentStore $contentProvider
-
 
102
         * @param int $validGenerators Bitmask which generators to allow (-1 = allow all)
88
         * @return void
103
         * @return void
89
         * @throws OIDplusException
104
         * @throws OIDplusException
90
         */
105
         */
91
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider) {
106
        private static function jwtSecurityCheck(OIDplusAuthContentStore $contentProvider, int $validGenerators=-1) {
92
                // Check if the token is intended for us
107
                // Check if the token is intended for us
93
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
108
                if ($contentProvider->getValue('aud','') !== OIDplus::getEditionInfo()['jwtaud']) {
94
                        throw new OIDplusException(_L('Token has wrong audience'));
109
                        throw new OIDplusException(_L('Token has wrong audience'));
95
                }
110
                }
96
                $gen = $contentProvider->getValue('oidplus_generator', -1);
111
                $gen = $contentProvider->getValue('oidplus_generator', -1);
Line 107... Line 122...
107
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
122
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
108
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
123
                                // Generator: plugins/viathinksoft/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
109
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
124
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
110
                        }
125
                        }
111
                }
126
                }
-
 
127
                else if ($gen === self::JWT_GENERATOR_REST) {
-
 
128
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_ADMIN', true)) {
-
 
129
                                // Generator: plugins/viathinksoft/adminPages/911_rest_api/OIDplusPageAdminRestApi.class.php
-
 
130
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_ADMIN'));
-
 
131
                        }
-
 
132
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_REST_USER', true)) {
-
 
133
                                // Generator: plugins/viathinksoft/raPages/911_rest_api/OIDplusPageRaRestApi.class.php
-
 
134
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_REST_USER'));
-
 
135
                        }
-
 
136
                }
112
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
137
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
113
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
138
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
114
                        // - No PHP session will be used
139
                        // - No PHP session will be used
115
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
140
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
116
                        // - No server-side session needed
141
                        // - No server-side session needed
Line 154... Line 179...
154
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
179
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
155
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
180
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
156
                        }
181
                        }
157
                }
182
                }
158
 
183
 
159
                // Checks which are dependent on the generator
184
                // Checks if JWT are dependent on the generator
160
                if ($gen === self::JWT_GENERATOR_LOGIN) {
185
                if ($validGenerators !== -1) {
161
                        if (!isset($_COOKIE[self::COOKIE_NAME])) {
-
 
162
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE'));
-
 
163
                        }
-
 
164
                }
-
 
165
                if ($gen === self::JWT_GENERATOR_AJAX) {
186
                        if (($gen & $validGenerators) === 0) {
166
                        if (!isset($_GET[self::COOKIE_NAME]) && !isset($_POST[self::COOKIE_NAME])) {
-
 
167
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST'));
187
                                throw new OIDplusException(_L('This kind of JWT token (%1) cannot be used in this request type', self::generatorName($gen)));
168
                        }
-
 
169
                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
-
 
170
                                throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php'));
-
 
171
                        }
188
                        }
172
                }
189
                }
173
        }
190
        }
174
 
191
 
175
        // Override abstract functions
192
        // Override abstract functions
Line 248... Line 265...
248
         * @return OIDplusAuthContentStore|null
265
         * @return OIDplusAuthContentStore|null
249
         * @throws OIDplusException
266
         * @throws OIDplusException
250
         */
267
         */
251
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
268
        public static function getActiveProvider()/*: ?OIDplusAuthContentStore*/ {
252
                if (!self::$contentProvider) {
269
                if (!self::$contentProvider) {
253
                        $jwt = '';
-
 
254
                        if (isset($_COOKIE[self::COOKIE_NAME])) $jwt = $_COOKIE[self::COOKIE_NAME];
-
 
255
                        if (isset($_POST[self::COOKIE_NAME]))   $jwt = $_POST[self::COOKIE_NAME];
-
 
256
                        if (isset($_GET[self::COOKIE_NAME]))    $jwt = $_GET[self::COOKIE_NAME];
-
 
257
 
270
 
258
                        if (!empty($jwt)) {
271
                        $tmp = null;
259
                                $tmp = new OIDplusAuthContentStoreJWT();
272
                        $silent_error = false;
260
 
273
 
261
                                try {
274
                        try {
262
                                        // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
-
 
263
                                        $tmp->loadJWT($jwt);
-
 
264
 
275
 
-
 
276
                                $rel_url = substr($_SERVER['REQUEST_URI'], strlen(OIDplus::webpath(null, OIDplus::PATH_RELATIVE_TO_ROOT)));
-
 
277
                                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.
-
 
278
 
265
                                        // Do various checks if the token is allowed and not blacklisted
279
                                        // REST may only use Bearer Authentication
266
                                        self::jwtSecurityCheck($tmp);
280
                                        $bearer = getBearerToken();
-
 
281
                                        if (!is_null($bearer)) {
-
 
282
                                                $silent_error = false;
-
 
283
                                                $tmp = new OIDplusAuthContentStoreJWT();
-
 
284
                                                $tmp->loadJWT($bearer);
-
 
285
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_REST | self::JWT_GENERATOR_MANUAL);
-
 
286
                                        }
-
 
287
 
-
 
288
                                } else {
-
 
289
 
-
 
290
                                        // A web-visitor (HTML and AJAX, but not REST) can use a JWT "remember me" Cookie
-
 
291
                                        if (isset($_COOKIE[self::COOKIE_NAME])) {
-
 
292
                                                $silent_error = true;
-
 
293
                                                $tmp = new OIDplusAuthContentStoreJWT();
-
 
294
                                                $tmp->loadJWT($_COOKIE[self::COOKIE_NAME]);
-
 
295
                                                self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_LOGIN | self::JWT_GENERATOR_MANUAL);
-
 
296
                                        }
-
 
297
 
-
 
298
                                        // AJAX may additionally use GET/POST automated AJAX (in addition to the normal JWT "remember me" Cookie)
-
 
299
                                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
-
 
300
                                                if (isset($_POST[self::COOKIE_NAME])) {
-
 
301
                                                        $silent_error = false;
-
 
302
                                                        $tmp = new OIDplusAuthContentStoreJWT();
-
 
303
                                                        $tmp->loadJWT($_POST[self::COOKIE_NAME]);
-
 
304
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
-
 
305
                                                }
-
 
306
                                                if (isset($_GET[self::COOKIE_NAME])) {
-
 
307
                                                        $silent_error = false;
-
 
308
                                                        $tmp = new OIDplusAuthContentStoreJWT();
-
 
309
                                                        $tmp->loadJWT($_GET[self::COOKIE_NAME]);
-
 
310
                                                        self::jwtSecurityCheck($tmp, self::JWT_GENERATOR_AJAX | self::JWT_GENERATOR_MANUAL);
-
 
311
                                                }
-
 
312
                                        }
-
 
313
 
-
 
314
                                }
-
 
315
 
267
                                } catch (\Exception $e) {
316
                        } catch (\Exception $e) {
268
                                        if (isset($_GET[self::COOKIE_NAME]) || isset($_POST[self::COOKIE_NAME])) {
317
                                if (!$silent_error) {
269
                                                // Most likely an AJAX request. We can throw an Exception
318
                                        // Most likely an AJAX request. We can throw an Exception
270
                                                throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
319
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
271
                                        } else {
320
                                } else {
272
                                                // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
321
                                        // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
273
                                                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
322
                                        OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
Line 275... Line 324...
275
                                        }
324
                                }
276
                                }
325
                        }
277
 
326
 
278
                                self::$contentProvider = $tmp;
327
                        self::$contentProvider = $tmp;
279
                        }
328
                }
280
                }
-
 
281
 
329
 
282
                return self::$contentProvider;
330
                return self::$contentProvider;
283
        }
331
        }
284
 
332
 
285
        /**
333
        /**
Line 295... Line 343...
295
                        self::$contentProvider = $this;
343
                        self::$contentProvider = $this;
296
                } else {
344
                } else {
297
                        $gen = $this->getValue('oidplus_generator',-1);
345
                        $gen = $this->getValue('oidplus_generator',-1);
298
                        switch ($gen) {
346
                        switch ($gen) {
299
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
347
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
-
 
348
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
300
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
349
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
301
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
350
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
302
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
351
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
303
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
352
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
304
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
353
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
Line 325... Line 374...
325
                        self::$contentProvider = $this;
374
                        self::$contentProvider = $this;
326
                } else {
375
                } else {
327
                        $gen = $this->getValue('oidplus_generator',-1);
376
                        $gen = $this->getValue('oidplus_generator',-1);
328
                        switch ($gen) {
377
                        switch ($gen) {
329
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
378
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
-
 
379
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_REST :
330
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
380
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
331
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
381
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
332
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
382
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
333
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
383
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
334
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
384
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
Line 344... Line 394...
344
        }
394
        }
345
 
395
 
346
        // Individual functions
396
        // Individual functions
347
 
397
 
348
        /**
398
        /**
-
 
399
         * Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
349
         * @param string $jwt
400
         * @param string $jwt
350
         * @return void
401
         * @return void
351
         * @throws OIDplusException
402
         * @throws OIDplusException
352
         */
403
         */
353
        public function loadJWT(string $jwt) {
404
        public function loadJWT(string $jwt) {