Subversion Repositories oidplus

Rev

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

Rev 583 Rev 585
Line 46... Line 46...
46
                }
46
                }
47
                $a = substr($a, 0, $len*2);
47
                $a = substr($a, 0, $len*2);
48
                return hex2bin($a);
48
                return hex2bin($a);
49
        }
49
        }
50
 
50
 
51
        // JWT handling
51
        // Content provider
52
 
-
 
53
        const JWT_GENERATOR_AJAX   = 0;
-
 
54
        const JWT_GENERATOR_LOGIN  = 1;
-
 
55
        const JWT_GENERATOR_MANUAL = 2;
-
 
56
 
-
 
57
        private function jwtGetBlacklistConfigKey($gen, $sub) {
-
 
58
                // Note: Needs to be <= 50 characters!
-
 
59
                return 'jwt_blacklist_gen('.$gen.')_sub('.trim(base64_encode(md5($sub,true)),'=').')';
-
 
60
        }
-
 
61
 
-
 
62
        public function jwtBlacklist($gen, $sub) {
-
 
63
                $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub);
-
 
64
                $bl_time = time()-1;
-
 
65
 
-
 
66
                $gen_desc = 'Unknown';
-
 
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
 
-
 
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) {});
-
 
72
                OIDplus::config()->setValue($cfg, $bl_time);
-
 
73
        }
-
 
74
 
-
 
75
        public function jwtGetBlacklistTime($gen, $sub) {
-
 
76
                $cfg = $this->jwtGetBlacklistConfigKey($gen, $sub);
-
 
77
                return OIDplus::config()->getValue($cfg,0);
-
 
78
        }
-
 
79
 
-
 
80
        protected function jwtSecurityCheck($contentProvider) {
-
 
81
                // Check if the token is intended for us
-
 
82
                if ($contentProvider->getValue('aud','') !== "http://oidplus.com") {
-
 
83
                        throw new OIDplusException(_L('Token has wrong audience'));
-
 
84
                }
-
 
85
                $gen = $contentProvider->getValue('oidplus_generator', -1);
-
 
86
                $sub = $contentProvider->getValue('sub', '');
-
 
87
 
-
 
88
                // Check if the token generator is allowed
-
 
89
                if ($gen === self::JWT_GENERATOR_AJAX) {
-
 
90
                        if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_ADMIN', true)) {
-
 
91
                                // Generator: plugins/adminPages/910_automated_ajax_calls/OIDplusPageAdminAutomatedAJAXCalls.class.php
-
 
92
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_ADMIN'));
-
 
93
                        }
-
 
94
                        else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_AJAX_USER', true)) {
-
 
95
                                // Generator: plugins/raPages/910_automated_ajax_calls/OIDplusPageRaAutomatedAJAXCalls.class.php
-
 
96
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_AJAX_USER'));
-
 
97
                        }
-
 
98
                }
-
 
99
                else if ($gen === self::JWT_GENERATOR_LOGIN) {
-
 
100
                        // Used for feature "Remember me" (use JWT token in a cookie as alternative to PHP session):
-
 
101
                        // - No PHP session will be used
-
 
102
                        // - Session will not be bound to IP address (therefore, you can switch between mobile/WiFi for example)
-
 
103
                        // - No server-side session needed
-
 
104
                        if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
-
 
105
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
-
 
106
                        }
-
 
107
                        else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
-
 
108
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
-
 
109
                        }
-
 
110
                }
-
 
111
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
-
 
112
                        // Generator 2 are "hand-crafted" tokens
-
 
113
                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', true)) {
-
 
114
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
-
 
115
                        }
-
 
116
                } else {
-
 
117
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
-
 
118
                }
-
 
119
 
-
 
120
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
-
 
121
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
-
 
122
                $bl_time = $this->jwtGetBlacklistTime($gen, $sub);
-
 
123
                $iat = $contentProvider->getValue('iat',0);
-
 
124
                if ($iat <= $bl_time) {
-
 
125
                        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)));
-
 
126
                }
-
 
127
 
-
 
128
                // Optional feature: Limit the JWT to a specific IP address
-
 
129
                // This could become handy if JWTs are used instead of Login sessions,
-
 
130
                // and you want to avoid session/JWT hijacking
-
 
131
                $ip = $contentProvider->getValue('ip','');
-
 
132
                if ($ip !== '') {
-
 
133
                        if (isset($_SERVER['REMOTE_ADDR']) && ($ip !== $_SERVER['REMOTE_ADDR'])) {
-
 
134
                                throw new OIDplusException(_L('Your IP address is not allowed to use this token'));
-
 
135
                        }
-
 
136
                }
-
 
137
 
52
 
138
                // Checks which are dependent on the generator
53
        public function getAuthMethod() {
139
                if ($gen === self::JWT_GENERATOR_LOGIN) {
54
                $acs = $this->getAuthContentStore();
140
                        if (!isset($_COOKIE['OIDPLUS_AUTH_JWT'])) {
55
                if (is_null($acs)) return 'null';
141
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','COOKIE'));
-
 
142
                        }
-
 
143
                }
-
 
144
                if ($gen === self::JWT_GENERATOR_AJAX) {
56
                return get_class($acs);
145
                        if (!isset($_GET['OIDPLUS_AUTH_JWT']) && !isset($_POST['OIDPLUS_AUTH_JWT'])) {
-
 
146
                                throw new OIDplusException(_L('This kind of JWT token can only be used with the %1 request type','GET/POST'));
-
 
147
                        }
-
 
148
                        if (isset($_SERVER['SCRIPT_FILENAME']) && (strtolower(basename($_SERVER['SCRIPT_FILENAME'])) !== 'ajax.php')) {
-
 
149
                                throw new OIDplusException(_L('This kind of JWT token can only be used in ajax.php'));
-
 
150
                        }
-
 
151
                }
57
        }
152
        }
-
 
153
 
-
 
154
        // Content provider
-
 
155
 
58
 
156
        protected function getAuthContentStore() {
59
        protected function getAuthContentStore() {
-
 
60
                // Logged in via JWT
-
 
61
                $tmp = OIDplusAuthContentStoreJWT::getActiveProvider();
157
                static $contentProvider = null;
62
                if ($tmp) return $tmp;
158
 
63
 
159
                if (is_null($contentProvider)) {
-
 
160
                        $jwt = '';
-
 
161
                        if (isset($_COOKIE['OIDPLUS_AUTH_JWT'])) $jwt = $_COOKIE['OIDPLUS_AUTH_JWT'];
-
 
162
                        if (isset($_POST['OIDPLUS_AUTH_JWT']))   $jwt = $_POST['OIDPLUS_AUTH_JWT'];
-
 
163
                        if (isset($_GET['OIDPLUS_AUTH_JWT']))    $jwt = $_GET['OIDPLUS_AUTH_JWT'];
-
 
164
 
-
 
165
                        if (!empty($jwt)) {
-
 
166
                                $contentProvider = new OIDplusAuthContentStoreJWT();
-
 
167
 
-
 
168
                                try {
-
 
169
                                        // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
-
 
170
                                        $contentProvider->loadJWT($jwt);
-
 
171
 
-
 
172
                                        // Do various checks if the token is allowed and not blacklisted
-
 
173
                                        $this->jwtSecurityCheck($contentProvider);
-
 
174
                                } catch (Exception $e) {
-
 
175
                                        if (isset($_GET['OIDPLUS_AUTH_JWT']) || isset($_POST['OIDPLUS_AUTH_JWT'])) {
-
 
176
                                                // Most likely an AJAX request. We can throw an Exception
-
 
177
                                                $contentProvider = null;
-
 
178
                                                throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
-
 
179
                                        } else {
-
 
180
                                                // Most likely an expired Cookie/Login session. We must not throw an Exception, otherwise we will break jsTree
-
 
181
                                                $contentProvider = new OIDplusAuthContentStoreSession();
-
 
182
                                                OIDplus::cookieUtils()->unsetcookie('OIDPLUS_AUTH_JWT');
-
 
183
                                        }
-
 
184
                                }
-
 
185
                        } else {
-
 
186
                                // Normal login via web-browser
64
                // Normal login via web-browser
-
 
65
                // Cookie will only be created once content is stored
187
                                $contentProvider = new OIDplusAuthContentStoreSession();
66
                $tmp = OIDplusAuthContentStoreSession::getActiveProvider();
-
 
67
                if ($tmp) return $tmp;
188
                        }
68
 
-
 
69
                // No active session and no JWT token available. User is not logged in.
-
 
70
                return null;
189
                }
71
        }
190
 
72
 
-
 
73
        public function getExtendedAttribute($name, $default=NULL) {
-
 
74
                $acs = $this->getAuthContentStore();
-
 
75
                if (is_null($acs)) return $default;
191
                return $contentProvider;
76
                return $acs->getValue($name, $default);
192
        }
77
        }
193
 
78
 
194
        // RA authentication functions
79
        // RA authentication functions
195
 
80
 
196
        public function raLogin($email) {
81
        public function raLogin($email) {
197
                return $this->getAuthContentStore()->raLogin($email);
82
                $acs = $this->getAuthContentStore();
-
 
83
                if (is_null($acs)) return;
-
 
84
                return $acs->raLogin($email);
198
        }
85
        }
199
 
86
 
200
        public function raLogout($email) {
87
        public function raLogout($email) {
201
                return $this->getAuthContentStore()->raLogout($email);
88
                $acs = $this->getAuthContentStore();
-
 
89
                if (is_null($acs)) return;
-
 
90
                return $acs->raLogout($email);
202
        }
91
        }
203
 
92
 
204
        public function raCheckPassword($ra_email, $password) {
93
        public function raCheckPassword($ra_email, $password) {
205
                $ra = new OIDplusRA($ra_email);
94
                $ra = new OIDplusRA($ra_email);
206
 
95
 
Line 216... Line 105...
216
 
105
 
217
                return false;
106
                return false;
218
        }
107
        }
219
 
108
 
220
        public function raNumLoggedIn() {
109
        public function raNumLoggedIn() {
221
                return $this->getAuthContentStore()->raNumLoggedIn();
110
                $acs = $this->getAuthContentStore();
222
        }
-
 
223
 
-
 
224
        public function raLogoutAll() {
111
                if (is_null($acs)) return 0;
225
                return $this->getAuthContentStore()->raLogoutAll();
112
                return $acs->raNumLoggedIn();
226
        }
113
        }
227
 
114
 
228
        public function loggedInRaList() {
115
        public function loggedInRaList() {
229
                if (OIDplus::authUtils()->forceAllLoggedOut()) {
116
                if ($this->forceAllLoggedOut()) {
230
                        return array();
117
                        return array();
231
                } else {
118
                } else {
232
                                return $this->getAuthContentStore()->loggedInRaList();
119
                        $acs = $this->getAuthContentStore();
-
 
120
                        if (is_null($acs)) return array();
-
 
121
                        return $acs->loggedInRaList();
233
                }
122
                }
234
        }
123
        }
235
 
124
 
236
        public function isRaLoggedIn($email) {
125
        public function isRaLoggedIn($email) {
-
 
126
                $acs = $this->getAuthContentStore();
-
 
127
                if (is_null($acs)) return false;
237
                        return $this->getAuthContentStore()->isRaLoggedIn($email);
128
                return $acs->isRaLoggedIn($email);
-
 
129
        }
-
 
130
 
-
 
131
        // "High level" function including logging and checking for valid JWT alternations
-
 
132
        public function raLoginEx($email, $remember_me, $origin) {
-
 
133
                $loginfo = '';
-
 
134
                $acs = $this->getAuthContentStore();
-
 
135
                if (!is_null($acs)) {
-
 
136
                        $acs->raLoginEx($email, $loginfo);
-
 
137
                        $acs->activate();
-
 
138
                } else {
-
 
139
                        if ($remember_me) {
-
 
140
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
-
 
141
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
-
 
142
                                }
-
 
143
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_USER', 10*365*24*60*60);
-
 
144
                                $authSimulation = new OIDplusAuthContentStoreJWT();
-
 
145
                                $authSimulation->raLoginEx($email, $loginfo);
-
 
146
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
-
 
147
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
-
 
148
                                $authSimulation->activate();
-
 
149
                        } else {
-
 
150
                                $authSimulation = new OIDplusAuthContentStoreSession();
-
 
151
                                $authSimulation->raLoginEx($email, $loginfo);
-
 
152
                                $authSimulation->activate();
-
 
153
                        }
-
 
154
                }
-
 
155
                $logmsg = "RA '$email' logged in";
-
 
156
                if ($origin != '') $logmsg .= " via $origin";
-
 
157
                if ($loginfo != '') $logmsg .= " ($loginfo)";
-
 
158
                OIDplus::logger()->log("[OK]RA($email)!", $logmsg);
-
 
159
        }
-
 
160
 
-
 
161
        public function raLogoutEx($email) {
-
 
162
                $loginfo = '';
-
 
163
 
-
 
164
                $acs = $this->getAuthContentStore();
-
 
165
                if (is_null($acs)) return;
-
 
166
                $res = $acs->raLogoutEx($email, $loginfo);
-
 
167
 
-
 
168
                OIDplus::logger()->log("[OK]RA($email)!", "RA '$email' logged out ($loginfo)");
-
 
169
 
-
 
170
                if (($this->raNumLoggedIn() == 0) && (!$this->isAdminLoggedIn())) {
-
 
171
                        // Nobody logged in anymore. Destroy session cookie to make GDPR people happy
-
 
172
                        $acs->destroySession();
-
 
173
                } else {
-
 
174
                        // Get a new token for the remaining users
-
 
175
                        $acs->activate();
-
 
176
                }
-
 
177
 
-
 
178
                return $res;
238
        }
179
        }
239
 
180
 
240
        // Admin authentication functions
181
        // Admin authentication functions
241
 
182
 
242
        public function adminLogin() {
183
        public function adminLogin() {
243
                return $this->getAuthContentStore()->adminLogin();
184
                $acs = $this->getAuthContentStore();
-
 
185
                if (is_null($acs)) return;
-
 
186
                return $acs->adminLogin();
244
        }
187
        }
245
 
188
 
246
        public function adminLogout() {
189
        public function adminLogout() {
247
                return $this->getAuthContentStore()->adminLogout();
190
                $acs = $this->getAuthContentStore();
-
 
191
                if (is_null($acs)) return;
-
 
192
                return $acs->adminLogout();
248
        }
193
        }
249
 
194
 
250
        public function adminCheckPassword($password) {
195
        public function adminCheckPassword($password) {
251
                $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
196
                $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
252
                if (empty($passwordData)) {
197
                if (empty($passwordData)) {
Line 268... Line 213...
268
                }
213
                }
269
                return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
214
                return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
270
        }
215
        }
271
 
216
 
272
        public function isAdminLoggedIn() {
217
        public function isAdminLoggedIn() {
273
                if (OIDplus::authUtils()->forceAllLoggedOut()) {
218
                if ($this->forceAllLoggedOut()) {
274
                        return false;
219
                        return false;
275
                } else {
220
                } else {
276
                                return $this->getAuthContentStore()->isAdminLoggedIn();
221
                        $acs = $this->getAuthContentStore();
-
 
222
                        if (is_null($acs)) return false;
-
 
223
                        return $acs->isAdminLoggedIn();
-
 
224
                }
-
 
225
        }
-
 
226
 
-
 
227
        // "High level" function including logging and checking for valid JWT alternations
-
 
228
        public function adminLoginEx($remember_me, $origin) {
-
 
229
                $loginfo = '';
-
 
230
                $acs = $this->getAuthContentStore();
-
 
231
                if (!is_null($acs)) {
-
 
232
                        $acs->adminLoginEx($loginfo);
-
 
233
                        $acs->activate();
-
 
234
                } else {
-
 
235
                        if ($remember_me) {
-
 
236
                                if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
-
 
237
                                        throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
-
 
238
                                }
-
 
239
                                $ttl = OIDplus::baseConfig()->getValue('JWT_TTL_LOGIN_ADMIN', 10*365*24*60*60);
-
 
240
                                $authSimulation = new OIDplusAuthContentStoreJWT();
-
 
241
                                $authSimulation->adminLoginEx($loginfo);
-
 
242
                                $authSimulation->setValue('oidplus_generator', OIDplusAuthContentStoreJWT::JWT_GENERATOR_LOGIN);
-
 
243
                                $authSimulation->setValue('exp', time()+$ttl); // JWT "exp" attribute
-
 
244
                                $authSimulation->activate();
-
 
245
                        } else {
-
 
246
                                $authSimulation = new OIDplusAuthContentStoreSession();
-
 
247
                                $authSimulation->adminLoginEx($loginfo);
-
 
248
                                $authSimulation->activate();
-
 
249
                        }
-
 
250
                }
-
 
251
                $logmsg = "Admin logged in";
-
 
252
                if ($origin != '') $logmsg .= " via $origin";
-
 
253
                if ($loginfo != '') $logmsg .= " ($loginfo)";
-
 
254
                OIDplus::logger()->log("[OK]A!", $logmsg);
277
                }
255
        }
-
 
256
 
-
 
257
        public function adminLogoutEx() {
-
 
258
                $loginfo = '';
-
 
259
 
-
 
260
                $acs = $this->getAuthContentStore();
-
 
261
                if (is_null($acs)) return;
-
 
262
                $res = $acs->adminLogoutEx($loginfo);
-
 
263
 
-
 
264
                if ($this->raNumLoggedIn() == 0) {
-
 
265
                        // Nobody here anymore. Destroy the cookie to make GDPR people happy
-
 
266
                        $acs->destroySession();
-
 
267
                } else {
-
 
268
                        // Get a new token for the remaining users
-
 
269
                        $acs->activate();
-
 
270
                }
-
 
271
 
-
 
272
                OIDplus::logger()->log("[OK]A!", "Admin logged out ($loginfo)");
-
 
273
                return $res;
278
        }
274
        }
279
 
275
 
280
        // Authentication keys for validating arguments (e.g. sent by mail)
276
        // Authentication keys for validating arguments (e.g. sent by mail)
281
 
277
 
282
        public static function makeAuthKey($data) {
278
        public static function makeAuthKey($data) {
Line 289... Line 285...
289
                return strcmp(self::makeAuthKey($data), $auth_key) === 0;
285
                return strcmp(self::makeAuthKey($data), $auth_key) === 0;
290
        }
286
        }
291
 
287
 
292
        // "Veto" functions to force logout state
288
        // "Veto" functions to force logout state
293
 
289
 
294
        public static function forceAllLoggedOut() {
290
        protected function forceAllLoggedOut() {
295
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
291
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
296
                        // The sitemap may not contain any confidential information,
292
                        // The sitemap may not contain any confidential information,
297
                        // even if the user is logged in, because the admin could
293
                        // even if the user is logged in, because the admin could
298
                        // accidentally copy-paste the sitemap to a
294
                        // accidentally copy-paste the sitemap to a
299
                        // search engine control panel while they are logged in
295
                        // search engine control panel while they are logged in
Line 340... Line 336...
340
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
336
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
341
        }
337
        }
342
 
338
 
343
        // Generate admin password
339
        // Generate admin password
344
 
340
 
345
        /* Nothing here; the admin password will be generated in setup_base.js */
341
        /* Nothing here; the admin password will be generated in setup_base.js , purely in the web-browser */
346
 
342
 
347
}
343
}