Subversion Repositories oidplus

Rev

Rev 576 | Rev 578 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
2 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
511 daniel-mar 5
 * Copyright 2019 - 2021 Daniel Marschall, ViaThinkSoft
2 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (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
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
511 daniel-mar 20
if (!defined('INSIDE_OIDPLUS')) die();
21
 
2 daniel-mar 22
class OIDplusAuthUtils {
23
 
566 daniel-mar 24
        // Useful functions
25
 
457 daniel-mar 26
        public static function getRandomBytes($len) {
27
                if (function_exists('openssl_random_pseudo_bytes')) {
28
                        $a = openssl_random_pseudo_bytes($len);
29
                        if ($a) return $a;
30
                }
31
 
465 daniel-mar 32
                if (function_exists('mcrypt_create_iv')) {
466 daniel-mar 33
                        $a = bin2hex(mcrypt_create_iv($len, MCRYPT_DEV_URANDOM));
465 daniel-mar 34
                        if ($a) return $a;
35
                }
36
 
457 daniel-mar 37
                if (function_exists('random_bytes')) {
38
                        $a = random_bytes($len);
39
                        if ($a) return $a;
40
                }
41
 
42
                // Fallback to non-secure RNG
43
                $a = '';
44
                while (strlen($a) < $len*2) {
45
                        $a .= sha1(uniqid(mt_rand(), true));
46
                }
47
                $a = substr($a, 0, $len*2);
48
                return hex2bin($a);
49
        }
50
 
577 daniel-mar 51
        // JWT handling
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';
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
                        // Reserved for future use (use JWT token in a cookie as alternative to PHP session):
101
                        if (($sub === 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_ADMIN', true)) {
102
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_ADMIN'));
103
                        }
104
                        else if (($sub !== 'admin') && !OIDplus::baseConfig()->getValue('JWT_ALLOW_LOGIN_USER', true)) {
105
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_LOGIN_USER'));
106
                        }
107
                } */
108
                else if ($gen === self::JWT_GENERATOR_MANUAL) {
109
                        // Generator 2 are "hand-crafted" tokens
110
                        if (!OIDplus::baseConfig()->getValue('JWT_ALLOW_MANUAL', true)) {
111
                                throw new OIDplusException(_L('The administrator has disabled this feature. (Base configuration setting %1).','JWT_ALLOW_MANUAL'));
112
                        }
113
                } else {
114
                        throw new OIDplusException(_L('Token generator %1 not recognized',$gen));
115
                }
116
 
117
                // Make sure that the IAT (issued at time) isn't in a blacklisted timeframe
118
                // When an user believes that a token was compromised, then they can blacklist the tokens identified by their "iat" ("Issued at") property
119
                $bl_time = $this->jwtGetBlacklistTime($gen, $sub);
120
                $iat = $contentProvider->getValue('iat',0);
121
                if ($iat <= $bl_time) {
122
                        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)));
123
                }
124
        }
125
 
566 daniel-mar 126
        // Content provider
2 daniel-mar 127
 
566 daniel-mar 128
        protected function getAuthContentStore() {
129
                static $contentProvider = null;
2 daniel-mar 130
 
566 daniel-mar 131
                if (is_null($contentProvider)) {
132
                        if (isset($_REQUEST['OIDPLUS_AUTH_JWT'])) {
133
                                $contentProvider = new OIDplusAuthContentStoreJWT();
572 daniel-mar 134
 
576 daniel-mar 135
                                try {
577 daniel-mar 136
                                        // Decode the JWT. In this step, the signature as well as EXP/NBF times will be checked
576 daniel-mar 137
                                        $contentProvider->loadJWT($_REQUEST['OIDPLUS_AUTH_JWT']);
577 daniel-mar 138
 
139
                                        // Do various checks if the token is allowed and not blacklisted
140
                                        $this->jwtSecurityCheck($contentProvider);
576 daniel-mar 141
                                } catch (Exception $e) {
142
                                        throw new OIDplusException(_L('The JWT token was rejected: %1',$e->getMessage()));
143
                                }
566 daniel-mar 144
                        } else {
570 daniel-mar 145
                                // Normal login via web-browser
566 daniel-mar 146
                                $contentProvider = new OIDplusAuthContentStoreSession();
147
                        }
148
                }
2 daniel-mar 149
 
566 daniel-mar 150
                return $contentProvider;
2 daniel-mar 151
        }
152
 
566 daniel-mar 153
        // RA authentication functions
2 daniel-mar 154
 
566 daniel-mar 155
        public function raLogin($email) {
156
                return $this->getAuthContentStore()->raLogin($email);
157
        }
2 daniel-mar 158
 
566 daniel-mar 159
        public function raLogout($email) {
160
                return $this->getAuthContentStore()->raLogout($email);
2 daniel-mar 161
        }
162
 
566 daniel-mar 163
        public function raCheckPassword($ra_email, $password) {
329 daniel-mar 164
                $ra = new OIDplusRA($ra_email);
453 daniel-mar 165
 
459 daniel-mar 166
                $authInfo = $ra->getAuthInfo();
453 daniel-mar 167
 
168
                $plugins = OIDplus::getAuthPlugins();
169
                if (count($plugins) == 0) {
170
                        throw new OIDplusException(_L('No RA authentication plugins found'));
171
                }
172
                foreach ($plugins as $plugin) {
459 daniel-mar 173
                        if ($plugin->verify($authInfo, $password)) return true;
453 daniel-mar 174
                }
175
 
176
                return false;
329 daniel-mar 177
        }
178
 
566 daniel-mar 179
        public function raNumLoggedIn() {
180
                return $this->getAuthContentStore()->raNumLoggedIn();
85 daniel-mar 181
        }
182
 
566 daniel-mar 183
        public function raLogoutAll() {
184
                return $this->getAuthContentStore()->raLogoutAll();
2 daniel-mar 185
        }
186
 
566 daniel-mar 187
        public function loggedInRaList() {
567 daniel-mar 188
                if (OIDplus::authUtils()->forceAllLoggedOut()) {
189
                        return array();
190
                } else {
191
                        return $this->getAuthContentStore()->loggedInRaList();
192
                }
2 daniel-mar 193
        }
194
 
566 daniel-mar 195
        public function isRaLoggedIn($email) {
196
                return $this->getAuthContentStore()->isRaLoggedIn($email);
2 daniel-mar 197
        }
198
 
14 daniel-mar 199
        // Admin authentication functions
2 daniel-mar 200
 
566 daniel-mar 201
        public function adminLogin() {
202
                return $this->getAuthContentStore()->adminLogin();
2 daniel-mar 203
        }
204
 
566 daniel-mar 205
        public function adminLogout() {
206
                return $this->getAuthContentStore()->adminLogout();
2 daniel-mar 207
        }
208
 
566 daniel-mar 209
        public function adminCheckPassword($password) {
421 daniel-mar 210
                $passwordData = OIDplus::baseConfig()->getValue('ADMIN_PASSWORD', '');
211
                if (empty($passwordData)) {
360 daniel-mar 212
                        throw new OIDplusException(_L('No admin password set in %1','userdata/baseconfig/config.inc.php'));
261 daniel-mar 213
                }
456 daniel-mar 214
 
421 daniel-mar 215
                if (strpos($passwordData, '$') !== false) {
456 daniel-mar 216
                        if ($passwordData[0] == '$') {
217
                                // Version 3: BCrypt
218
                                return password_verify($password, $passwordData);
219
                        } else {
457 daniel-mar 220
                                // Version 2: SHA3-512 with salt
456 daniel-mar 221
                                list($s_salt, $hash) = explode('$', $passwordData, 2);
222
                        }
421 daniel-mar 223
                } else {
456 daniel-mar 224
                        // Version 1: SHA3-512 without salt
421 daniel-mar 225
                        $s_salt = '';
226
                        $hash = $passwordData;
227
                }
228
                return strcmp(sha3_512($s_salt.$password, true), base64_decode($hash)) === 0;
2 daniel-mar 229
        }
230
 
566 daniel-mar 231
        public function isAdminLoggedIn() {
567 daniel-mar 232
                if (OIDplus::authUtils()->forceAllLoggedOut()) {
233
                        return false;
234
                } else {
235
                        return $this->getAuthContentStore()->isAdminLoggedIn();
236
                }
2 daniel-mar 237
        }
238
 
310 daniel-mar 239
        // Authentication keys for validating arguments (e.g. sent by mail)
2 daniel-mar 240
 
241
        public static function makeAuthKey($data) {
424 daniel-mar 242
                $data = OIDplus::baseConfig()->getValue('SERVER_SECRET') . '/AUTHKEY/' . $data;
421 daniel-mar 243
                $calc_authkey = sha3_512($data, false);
2 daniel-mar 244
                return $calc_authkey;
245
        }
246
 
247
        public static function validateAuthKey($data, $auth_key) {
421 daniel-mar 248
                return strcmp(self::makeAuthKey($data), $auth_key) === 0;
2 daniel-mar 249
        }
250
 
329 daniel-mar 251
        // "Veto" functions to force logout state
252
 
253
        public static function forceAllLoggedOut() {
254
                if (isset($_SERVER['SCRIPT_FILENAME']) && (basename($_SERVER['SCRIPT_FILENAME']) == 'sitemap.php')) {
255
                        // The sitemap may not contain any confidential information,
256
                        // even if the user is logged in, because the admin could
257
                        // accidentally copy-paste the sitemap to a
258
                        // search engine control panel while they are logged in
259
                        return true;
260
                } else {
261
                        return false;
262
                }
263
        }
427 daniel-mar 264
 
424 daniel-mar 265
        // CSRF functions
427 daniel-mar 266
 
424 daniel-mar 267
        private $enable_csrf = true;
427 daniel-mar 268
 
424 daniel-mar 269
        public function enableCSRF() {
270
                $this->enable_csrf = true;
271
        }
427 daniel-mar 272
 
424 daniel-mar 273
        public function disableCSRF() {
274
                $this->enable_csrf = false;
275
        }
427 daniel-mar 276
 
424 daniel-mar 277
        public function genCSRFToken() {
564 daniel-mar 278
                return bin2hex(self::getRandomBytes(64));
424 daniel-mar 279
        }
427 daniel-mar 280
 
424 daniel-mar 281
        public function checkCSRF() {
427 daniel-mar 282
                if (!$this->enable_csrf) return;
564 daniel-mar 283
                if (!isset($_REQUEST['csrf_token']) || !isset($_COOKIE['csrf_token']) || ($_REQUEST['csrf_token'] !== $_COOKIE['csrf_token'])) {
284
                        throw new OIDplusException(_L('Wrong CSRF Token'));
424 daniel-mar 285
                }
286
        }
329 daniel-mar 287
 
421 daniel-mar 288
        // Generate RA passwords
289
 
461 daniel-mar 290
        public static function raGeneratePassword($password): OIDplusRAAuthInfo {
453 daniel-mar 291
                $def_method = OIDplus::config()->getValue('default_ra_auth_method');
292
 
293
                $plugins = OIDplus::getAuthPlugins();
294
                foreach ($plugins as $plugin) {
295
                        if (basename($plugin->getPluginDirectory()) === $def_method) {
296
                                return $plugin->generate($password);
297
                        }
298
                }
299
                throw new OIDplusException(_L('Default RA auth method/plugin "%1" not found',$def_method));
421 daniel-mar 300
        }
301
 
302
        // Generate admin password
303
 
304
        /* Nothing here; the admin password will be generated in setup_base.js */
305
 
306
}