Subversion Repositories oidplus

Rev

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

Rev 576 Rev 585
Line 19... Line 19...
19
 
19
 
20
if (!defined('INSIDE_OIDPLUS')) die();
20
if (!defined('INSIDE_OIDPLUS')) die();
21
 
21
 
22
class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy {
22
class OIDplusAuthContentStoreJWT extends OIDplusAuthContentStoreDummy {
23
 
23
 
-
 
24
        const COOKIE_NAME = 'OIDPLUS_AUTH_JWT';
-
 
25
 
-
 
26
        const JWT_GENERATOR_AJAX   = 0; // "Automated AJAX" plugin
-
 
27
        const JWT_GENERATOR_LOGIN  = 1; // "Remember me" login method
-
 
28
        const JWT_GENERATOR_MANUAL = 2; // "Manually crafted" JWT tokens
-
 
29
 
-
 
30
        private static function jwtGetBlacklistConfigKey($gen, $sub) {
-
 
31
                // Note: Needs to be <= 50 characters!
-
 
32
                return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
-
 
33
        }
-
 
34
 
-
 
35
        public static function jwtBlacklist($gen, $sub) {
-
 
36
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
-
 
37
                $bl_time = time()-1;
-
 
38
 
-
 
39
                $gen_desc = 'Unknown';
-
 
40
                if ($gen === self::JWT_GENERATOR_AJAX)   $gen_desc = 'Automated AJAX calls';
-
 
41
                if ($gen === self::JWT_GENERATOR_LOGIN)  $gen_desc = 'Login ("Remember me")';
-
 
42
                if ($gen === self::JWT_GENERATOR_MANUAL) $gen_desc = 'Manually created';
-
 
43
 
-
 
44
                OIDplus::config()->prepareConfigKey($cfg, 'Revoke timestamp of all JWT tokens for $sub with generator $gen ($gen_desc)', $bl_time, OIDplusConfig::PROTECTION_HIDDEN, function($value) {});
-
 
45
                OIDplus::config()->setValue($cfg, $bl_time);
-
 
46
        }
-
 
47
 
-
 
48
        public static function jwtGetBlacklistTime($gen, $sub) {
-
 
49
                $cfg = self::jwtGetBlacklistConfigKey($gen, $sub);
-
 
50
                return OIDplus::config()->getValue($cfg,0);
-
 
51
        }
-
 
52
 
-
 
53
        private static function jwtSecurityCheck($contentProvider) {
-
 
54
                // Check if the token is intended for us
-
 
55
                if ($contentProvider->getValue('aud','') !== "http://oidplus.com") {
-
 
56
                        throw new OIDplusException(_L('Token has wrong audience'));
-
 
57
                }
-
 
58
                $gen = $contentProvider->getValue('oidplus_generator', -1);
-
 
59
 
-
 
60
                $has_admin = $contentProvider->isAdminLoggedIn();
-
 
61
                $has_ra = $contentProvider->raNumLoggedIn() > 0;
-
 
62
 
-
 
63
                // Check if the token generator is allowed
-
 
64
                if ($gen === self::JWT_GENERATOR_AJAX) {
-
 
65
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
-
 
66
                                // Generator: plugins/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
-
 
67
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
-
 
68
                        }
-
 
69
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
-
 
70
                                // Generator: plugins/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
-
 
71
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
-
 
72
                        }
-
 
73
                }
-
 
74
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
-
 
75
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
-
 
76
                        // - No PHP session will be used
-
 
77
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
-
 
78
                        // - No server-side session needed
-
 
79
                        if (($has_admin) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
-
 
80
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
-
 
81
                        }
-
 
82
                        if (($has_ra) && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
-
 
83
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
-
 
84
                        }
-
 
85
                }
-
 
86
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
-
 
87
                        // Generator 2 are "hand-crafted" tokens
-
 
88
                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', false)) {
-
 
89
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
-
 
90
                        }
-
 
91
                } else {
-
 
92
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
-
 
93
                }
-
 
94
 
-
 
95
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
-
 
96
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
-
 
97
                // When a user logs out of a "remember me" session, the JWT token will be blacklisted as well
-
 
98
                // Small side effect: All "remember me" sessions of that user will be revoked then
-
 
99
                $sublist = $contentProvider->loggedInRaList();
-
 
100
                foreach ($sublist as &$sub) {
-
 
101
                        $sub = $sub->raEmail();
-
 
102
                }
-
 
103
                if ($has_admin) $sublist[] = 'admin';
-
 
104
                foreach ($sublist as $sub) {
-
 
105
                        $bl_time = self::jwtGetBlacklistTime($gen, $sub);
-
 
106
                        $iat = $contentProvider->getValue('iat',0);
-
 
107
                        if ($iat <= $bl_time) {
-
 
108
                                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)));
-
 
109
                        }
-
 
110
                }
-
 
111
 
-
 
112
                // Optional feature: Limit the JWT to a specific IP address
-
 
113
                // Currently not used in OIDplus
-
 
114
                $ip = $contentProvider->getValue('ip','');
-
 
115
                if ($ip !== '') {
-
 
116
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
-
 
117
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
-
 
118
                        }
-
 
119
                }
-
 
120
 
-
 
121
                // Checks which are dependent on the generator
-
 
122
                if ($gen === self::JWT_GENERATOR_LOGIN) {
-
 
123
                        if (!isset($_COOKIE[self::COOKIE_NAME])) {
-
 
124
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE'));
-
 
125
                        }
-
 
126
                }
-
 
127
                if ($gen === self::JWT_GENERATOR_AJAX) {
-
 
128
                        if (!isset($_GET[self::COOKIE_NAME]) && !isset($_POST[self::COOKIE_NAME])) {
-
 
129
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST'));
-
 
130
                        }
-
 
131
                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
-
 
132
                                throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php'));
-
 
133
                        }
-
 
134
                }
-
 
135
        }
-
 
136
 
-
 
137
        // Override abstract functions
-
 
138
 
-
 
139
        public function activate() {
-
 
140
                $token = $this->getJWTToken();
-
 
141
                $exp = $this->getValue('exp',0);
-
 
142
                OIDplus::cookieUtils()->setcookie(self::COOKIE_NAME, $token, $exp, false);
-
 
143
        }
-
 
144
 
-
 
145
        public function destroySession() {
-
 
146
                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
-
 
147
        }
-
 
148
 
-
 
149
        public function raLogout($email) {
-
 
150
                $gen = $this->getValue('oidplus_generator', -1);
-
 
151
                if ($gen >= 0) self::jwtBlacklist($gen, $email);
-
 
152
                parent::raLogout($email);
-
 
153
        }
-
 
154
 
-
 
155
        public function raLogoutEx($email, &$loginfo) {
-
 
156
                $this->raLogout($email);
-
 
157
                $loginfo = 'from JWT session';
-
 
158
        }
-
 
159
 
-
 
160
        public function adminLogout() {
-
 
161
                $gen = $this->getValue('oidplus_generator', -1);
-
 
162
                if ($gen >= 0) self::jwtBlacklist($gen, 'admin');
-
 
163
                parent::adminLogout();
-
 
164
        }
-
 
165
 
-
 
166
        public function adminLogoutEx(&$loginfo) {
-
 
167
                $this->adminLogout();
-
 
168
                $loginfo = 'from JWT session';
-
 
169
        }
-
 
170
 
-
 
171
        public static function getActiveProvider() {
-
 
172
                static $contentProvider = null;
-
 
173
 
-
 
174
                if (!$contentProvider) {
-
 
175
                        $jwt = '';
-
 
176
                        if (isset($_COOKIE[self::COOKIE_NAME])) $jwt = $_COOKIE[self::COOKIE_NAME];
-
 
177
                        if (isset($_POST[self::COOKIE_NAME]))   $jwt = $_POST[self::COOKIE_NAME];
-
 
178
                        if (isset($_GET[self::COOKIE_NAME]))    $jwt = $_GET[self::COOKIE_NAME];
-
 
179
 
-
 
180
                        if (!empty($jwt)) {
-
 
181
                                $tmp = new OIDplusAuthContentStoreJWT();
-
 
182
 
-
 
183
                                try {
-
 
184
                                        // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
-
 
185
                                        $tmp->loadJWT($jwt);
-
 
186
 
-
 
187
                                        // Do various checks if the token is allowed and not blacklisted
-
 
188
                                        self::jwtSecurityCheck($tmp);
-
 
189
                                } catch (Exception $e) {
-
 
190
                                        if (isset($_GET[self::COOKIE_NAME]) || isset($_POST[self::COOKIE_NAME])) {
-
 
191
                                                // Most likely an AJAX request. We can throw an Exception
-
 
192
                                                throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
-
 
193
                                        } else {
-
 
194
                                                // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
-
 
195
                                                OIDplus::cookieUtils()->unsetcookie(self::COOKIE_NAME);
-
 
196
                                                return null;
-
 
197
                                        }
-
 
198
                                }
-
 
199
 
-
 
200
                                $contentProvider = $tmp;
-
 
201
                        }
-
 
202
                }
-
 
203
 
-
 
204
                return $contentProvider;
-
 
205
        }
-
 
206
 
-
 
207
        public function raLoginEx($email, &$loginfo) {
-
 
208
                if (is_null(self::getActiveProvider())) {
-
 
209
                        $this->raLogin($email);
-
 
210
                        $loginfo = 'into new JWT session';
-
 
211
                } else {
-
 
212
                        $gen = $this->getValue('oidplus_generator',-1);
-
 
213
                        switch ($gen) {
-
 
214
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
-
 
215
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
-
 
216
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
-
 
217
                                        break;
-
 
218
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
-
 
219
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
-
 
220
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
-
 
221
                                        }
-
 
222
                                        break;
-
 
223
                                default:
-
 
224
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
-
 
225
                                        break;
-
 
226
                        }
-
 
227
                        $this->raLogin($email);
-
 
228
                        $loginfo = 'into existing JWT session';
-
 
229
                }
-
 
230
        }
-
 
231
 
-
 
232
        public function adminLoginEx(&$loginfo) {
-
 
233
                if (is_null(self::getActiveProvider())) {
-
 
234
                        $this->adminLogin();
-
 
235
                        $loginfo = 'into new JWT session';
-
 
236
                } else {
-
 
237
                        $gen = $this->getValue('oidplus_generator',-1);
-
 
238
                        switch ($gen) {
-
 
239
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_AJAX :
-
 
240
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_MANUAL :
-
 
241
                                        throw new OIDplusException(_L('This kind of JWT token cannot be altered. Therefore you cannot do this action.'));
-
 
242
                                        break;
-
 
243
                                case OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN :
-
 
244
                                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
-
 
245
                                                throw new OIDplusException(_L('You cannot add this login credential to your existing "remember me" session. You need to log-out first.'));
-
 
246
                                        }
-
 
247
                                        break;
-
 
248
                                default:
-
 
249
                                        assert(false); // This cannot happen because jwtSecurityCheck will check for unknown generators
-
 
250
                                        break;
-
 
251
                        }
-
 
252
                        $this->adminLogin();
-
 
253
                        $loginfo = 'into existing JWT session';
-
 
254
                }
-
 
255
        }
-
 
256
 
24
        // Individual functions
257
        // Individual functions
25
 
258
 
26
        public function loadJWT($jwt) {
259
        public function loadJWT($jwt) {
27
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
260
                \Firebase\JWT\JWT::$leeway = 60; // leeway in seconds
28
                if (OIDplus::getPkiStatus()) {
261
                if (OIDplus::getPkiStatus()) {
Line 33... Line 266...
33
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 64/*256bit*/, false);
266
                        $key = hash_pbkdf2('sha512', $key, '', 10000, 64/*256bit*/, false);
34
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $key, array('HS256', 'HS384', 'HS512'));
267
                        $this->content = (array) \Firebase\JWT\JWT::decode($jwt, $key, array('HS256', 'HS384', 'HS512'));
35
                }
268
                }
36
        }
269
        }
37
 
270
 
38
        public function getJWTToken($lifetime=null) {
271
        public function getJWTToken() {
39
                $payload = $this->content;
272
                $payload = $this->content;
40
                $payload["iss"] = "http://oidplus.com";
273
                $payload["iss"] = "http://oidplus.com";
41
                $payload["aud"] = "http://oidplus.com";
274
                $payload["aud"] = "http://oidplus.com";
42
                $payload["jti"] = gen_uuid();
275
                $payload["jti"] = gen_uuid();
43
                $payload["iat"] = time();
276
                $payload["iat"] = time();
44
                if (!is_null($lifetime)) $payload["exp"] = time() + $lifetime;
-
 
45
 
277
 
46
                if (OIDplus::getPkiStatus()) {
278
                if (OIDplus::getPkiStatus()) {
47
                        $privKey = OIDplus::config()->getValue('oidplus_private_key');
279
                        $privKey = OIDplus::config()->getValue('oidplus_private_key');
48
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS512');
280
                        return \Firebase\JWT\JWT::encode($payload, $privKey, 'RS512');
49
                } else {
281
                } else {