WebAppAuthentication::setMAPISession()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
require_once UMAPI_PATH . '/mapi.util.php';
4
require_once UMAPI_PATH . '/class.keycloak.php';
5
6
require_once BASE_PATH . 'server/includes/core/class.encryptionstore.php';
7
require_once BASE_PATH . 'server/includes/core/class.webappsession.php';
8
require_once BASE_PATH . 'server/includes/core/class.mapisession.php';
9
require_once BASE_PATH . 'server/includes/core/class.browserfingerprint.php';
10
11
/*
12
* Class that handles authentication.
13
*
14
* @singleton
15
*/
16
class WebAppAuthentication {
17
	/**
18
	 * @var null|self A reference to the only instance of this class
19
	 */
20
	private static $_instance;
21
22
	/**
23
	 * @var bool|false True if the user is authenticated, false otherwise
24
	 */
25
	private static $_authenticated = false;
26
27
	/**
28
	 * @var null|WebAppSession A reference to the php session object
29
	 */
30
	private static $_phpSession;
31
32
	/**
33
	 * @var null|MAPISession A reference to the MAPISession object
34
	 */
35
	private static $_mapiSession;
36
37
	/**
38
	 * @var 0|int An code that reflects the latest error
0 ignored issues
show
Documentation Bug introduced by
The doc comment 0|int at position 0 could not be parsed: Unknown type name '0' at position 0 in 0|int.
Loading history...
39
	 *
40
	 * @see $UMAPI_PATH/mapicodes.php
41
	 */
42
	private static $_errorCode = NOERROR;
43
44
	/**
45
	 * @var bool True if MAPI session savng support exists
46
	 */
47
	private static $_sessionSaveSupport = false;
48
49
	/**
50
	 * Returns the only instance of the WebAppAuthentication class.
51
	 * If it does not exist yet, it will create an instance, and
52
	 * also an MAPISession object, and it will start a php session
53
	 * by instantiating a WebAppSession.
54
	 *
55
	 * @return self
56
	 */
57
	public static function getInstance() {
58
		if (is_null(WebAppAuthentication::$_instance)) {
59
			// Make sure a php session is started
60
			WebAppAuthentication::$_phpSession = WebAppSession::getInstance();
61
62
			// Instantiate this class
63
			WebAppAuthentication::$_instance = new WebAppAuthentication();
64
65
			// Instantiate the mapiSession
66
			WebAppAuthentication::$_mapiSession = new MAPISession();
67
68
			// Check if MAPI Saving session support exists
69
			WebAppAuthentication::$_sessionSaveSupport = function_exists('kc_session_save') && function_exists('kc_session_restore');
70
		}
71
72
		return WebAppAuthentication::$_instance;
73
	}
74
75
	/**
76
	 * Returns the error code of the last logon attempt.
77
	 *
78
	 * @return int
79
	 */
80
	public static function getErrorCode() {
81
		return WebAppAuthentication::$_errorCode;
82
	}
83
84
	/**
85
	 * Returns an error message that goed with the error code of
86
	 * the last logon attempt.
87
	 *
88
	 * @return string
89
	 */
90
	public static function getErrorMessage() {
91
		return match (WebAppAuthentication::getErrorCode()) {
92
			NOERROR => '',
93
			ecUnknownUser, MAPI_E_LOGON_FAILED, MAPI_E_UNCONFIGURED => _('Logon failed. Please verify your credentials and try again.'),
94
			MAPI_E_NETWORK_ERROR => _('Cannot connect to Gromox.'),
95
			MAPI_E_INVALID_WORKSTATION_ACCOUNT => _('Login did not work due to a duplicate session. The issue was automatically resolved, please log in again.'),
96
			MAPI_E_END_OF_SESSION => '',
97
			default => _('Unknown MAPI Error') . ': ' . get_mapi_error_name(WebAppAuthentication::getErrorCode()),
98
		};
99
	}
100
101
	/**
102
	 * Returns the MAPISession instance.
103
	 *
104
	 * @see server/includes/core/class.mapisession.php
105
	 *
106
	 * @return MAPISession
107
	 */
108
	public static function getMAPISession() {
109
		return WebAppAuthentication::$_mapiSession;
110
	}
111
112
	/**
113
	 * Set the MAPISession instance.
114
	 *
115
	 * @param MAPISession $session the mapisession to set
116
	 */
117
	public static function setMAPISession($session) {
118
		WebAppAuthentication::$_mapiSession->setSession($session);
0 ignored issues
show
Bug introduced by
The method setSession() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

118
		WebAppAuthentication::$_mapiSession->/** @scrutinizer ignore-call */ 
119
                                       setSession($session);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
	}
120
121
	/**
122
	 * Tries to authenticate the user. First it will check if the
123
	 * user is using the login-form. And finally if not of above
124
	 * methods apply, it will try to find credentials in the
125
	 * php session.
126
	 */
127
	public static function authenticate() {
128
		WebAppAuthentication::regenerate_access_token();
129
		if (isset($_GET['code']) && (!defined('DISABLE_KEYCLOAK') || !DISABLE_KEYCLOAK)) {
130
			WebAppAuthentication::authenticateWithAccessToken($_GET['code']);
131
		}
132
		elseif (WebAppAuthentication::isUsingLoginForm()) {
133
			WebAppAuthentication::authenticateWithPostedCredentials();
134
		}
135
		// At last check if we have credentials in the session
136
		// and if found, try to login with those
137
		else {
138
			WebAppAuthentication::_authenticateWithSession();
139
		}
140
	}
141
142
	/*
143
	* Checks if keycloak features are enabled and regenerates
144
	* the access token before expiration.
145
	*/
146
	public static function regenerate_access_token() {
147
		if (isset($_SESSION['_keycloak_auth'])) {
148
			$_keycloak_auth = $_SESSION['_keycloak_auth'];
149
			if (time() - $_keycloak_auth->get_last_refresh_time() > 280) {
150
				if (!$_keycloak_auth->refresh_grant_req() && !$_keycloak_auth->validate_grant()) {
151
					header('Location:' . $_keycloak_auth->login_url($_keycloak_auth->redirect_url) . '');
152
				}
153
				$token = $_keycloak_auth->access_token->get_payload();
154
				$user = $_keycloak_auth->access_token->get_claims('email');
155
				WebAppAuthentication::_storeCredentialsInSession($user, $token);
156
				$_keycloak_auth->set_last_refresh_time(time());
157
				$_SESSION['_keycloak_auth'] = $_keycloak_auth;
158
			}
159
		}
160
	}
161
162
	/**
163
	 * Returns true if a user is authenticated, or false otherwise.
164
	 *
165
	 * @return bool
166
	 */
167
	public static function isAuthenticated() {
168
		return WebAppAuthentication::$_authenticated;
169
	}
170
171
	/**
172
	 * Tries to logon to Gromox with the given username and password/token. Returns
173
	 * the error code that was given back.
174
	 *
175
	 * @param string $username The username
176
	 * @param string $pass     The password/token
177
	 *
178
	 * @return int
179
	 */
180
	public static function login($username, $pass) {
181
		if (!WebAppAuthentication::_restoreMAPISession()) {
182
			// TODO: move logon from MAPISession to here
183
184
			WebAppAuthentication::$_errorCode = isset($_SESSION['_keycloak_auth']) ?
185
				WebAppAuthentication::$_mapiSession->logon_token($username, $pass) :
186
				WebAppAuthentication::$_mapiSession->logon($username, $pass, DEFAULT_SERVER);
187
188
			// Include external login plugins to be loaded
189
			if (file_exists(BASE_PATH . 'extlogin.php')) {
190
				include BASE_PATH . 'extlogin.php';
191
			}
192
			if (WebAppAuthentication::$_errorCode === NOERROR) {
193
				WebAppAuthentication::$_authenticated = true;
194
				WebAppAuthentication::_storeMAPISession(WebAppAuthentication::$_mapiSession->getSession());
195
				$tmp = explode('@', $username);
196
				if (count($tmp) == 2) {
197
					setcookie('domainname', $tmp[1], ['expires' => time() + 31536000, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']);
198
				}
199
				$wa_title = WebAppAuthentication::$_mapiSession->getFullName();
200
				$companyname = WebAppAuthentication::$_mapiSession->getCompanyName();
201
				if (isset($companyname) && strlen($companyname) != 0) {
202
					$wa_title .= " ({$companyname})";
203
				}
204
				if (strlen($wa_title) != 0) {
205
					setcookie('webapp_title', $wa_title, ['expires' => time() + 31536000, 'path' => '/', 'domain' => '', 'secure' => true, 'httponly' => true, 'samesite' => 'Strict']);
206
				}
207
			}
208
			elseif (WebAppAuthentication::$_errorCode == MAPI_E_LOGON_FAILED || WebAppAuthentication::$_errorCode == MAPI_E_UNCONFIGURED) {
209
				error_log('grommunio Web user: ' . $username . ': authentication failure at MAPI');
210
			}
211
		}
212
213
		return WebAppAuthentication::$_errorCode;
214
	}
215
216
	/**
217
	 * Store a serialized MAPI Session, which can be used by _restoreMAPISession to re-create
218
	 * a MAPISession, which saves a login call.
219
	 *
220
	 * @param MAPISession $session the session to serialize and save
221
	 */
222
	private static function _storeMAPISession($session) {
223
		if (!WebAppAuthentication::$_sessionSaveSupport) {
224
			return;
225
		}
226
227
		$encryptionStore = EncryptionStore::getInstance();
228
		$data = '';
229
		if (kc_session_save($session, $data) === NOERROR) {
230
			$encryptionStore->add('savedsession', bin2hex((string) $data));
231
		}
232
	}
233
234
	/**
235
	 * Restore a MAPISession from the serialized with kc_session_restore.
236
	 *
237
	 * @return bool true if session has been restored successfully
238
	 */
239
	private static function _restoreMAPISession() {
240
		$encryptionStore = EncryptionStore::getInstance();
241
242
		if (!WebAppAuthentication::$_sessionSaveSupport || $encryptionStore->get('savedsession') === null) {
243
			return false;
244
		}
245
246
		if (kc_session_restore(hex2bin((string) $encryptionStore->get('savedsession')), $session) === NOERROR) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $session seems to be never defined.
Loading history...
247
			WebAppAuthentication::$_errorCode = NOERROR;
248
			WebAppAuthentication::$_authenticated = true;
249
			WebAppAuthentication::setMAPISession($session);
250
251
			return true;
252
		}
253
254
		return false;
255
	}
256
257
	/**
258
	 * Stores the given username and password in the session using the encryptionstore.
259
	 *
260
	 * @param string The username
261
	 * @param string The password
262
	 * @param mixed $username
263
	 * @param mixed $password
264
	 */
265
	private static function _storeCredentialsInSession($username, $password) {
266
		$encryptionStore = EncryptionStore::getInstance();
267
		$encryptionStore->add('username', $username);
268
		$encryptionStore->add('password', $password);
269
	}
270
271
	/**
272
	 * Checks if a user tries to log in by submitting the login form.
273
	 *
274
	 * @return bool
275
	 */
276
	public static function isUsingLoginForm() {
277
		// Login form is only found on index.php
278
		// If we don't check it, then posting to grommunio.php would
279
		// also make authenticating possible.
280
		if (basename((string) $_SERVER['SCRIPT_NAME']) !== 'index.php') {
281
			return false;
282
		}
283
284
		return isset($_POST) && isset($_POST['username'], $_POST['password']);
285
	}
286
287
	/**
288
	 * Tries to authenticate the user with credentials that were posted.
289
	 * Returns the error code from the logon attempt.
290
	 *
291
	 * @return int
292
	 */
293
	public static function authenticateWithPostedCredentials() {
294
		$email = appendDefaultDomain($_POST['username']);
295
		if (empty($email) || empty($_POST['password'])) {
296
			WebAppAuthentication::$_errorCode = MAPI_E_LOGON_FAILED;
297
298
			return WebAppAuthentication::getErrorCode();
299
		}
300
		// Check if a session is already running and if the credentials match
301
		$encryptionStore = EncryptionStore::getInstance();
302
		$username = $encryptionStore->get('username');
303
		$password = $encryptionStore->get('password');
304
305
		if (!is_null($username) && !is_null($password)) {
306
			if ($username != $email || $password != $_POST['password']) {
307
				WebAppAuthentication::$_errorCode = MAPI_E_INVALID_WORKSTATION_ACCOUNT;
308
				WebAppAuthentication::$_phpSession->destroy();
0 ignored issues
show
Bug introduced by
The method destroy() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

308
				WebAppAuthentication::$_phpSession->/** @scrutinizer ignore-call */ 
309
                                        destroy();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
309
310
				return WebAppAuthentication::getErrorCode();
311
			}
312
		}
313
		else {
314
			// If no session is currently running, then store a fingerprint of the requester
315
			// in the session.
316
			$_SESSION['fingerprint'] = BrowserFingerprint::getFingerprint();
317
		}
318
319
		// Give the session a new id
320
		session_regenerate_id();
321
322
		WebAppAuthentication::login($email, $_POST['password']);
323
324
		// Store the credentials in the session if logging in was successful
325
		if (WebAppAuthentication::$_errorCode === NOERROR) {
326
			WebAppAuthentication::_storeCredentialsInSession($email, $_POST['password']);
327
328
			return WebAppAuthentication::getErrorCode();
329
		}
330
331
		return WebAppAuthentication::getErrorCode();
332
	}
333
334
	/**
335
	 * Login with Oauth2.0 keycloak access token.
336
	 * User selects login with keycloak, then gets redirected to keycloak server.
337
	 * If login is successful, the user is redirected with code grant back to gromox server.
338
	 * gromox requests access token with the received grant.
339
	 * keycloak server verifies grant, and sends access token.
340
	 * access token is used to authenticate user.
341
	 *
342
	 * @param mixed $code
343
	 */
344
	public static function authenticateWithAccessToken($code) {
345
		$keycloak = KeyCloak::getInstance();
0 ignored issues
show
Bug introduced by
The type KeyCloak was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
346
		if (!is_null($keycloak)) {
347
			if ($keycloak->client_credential_grant_req($code) && $keycloak->validate_grant()) {
348
				$keycloak->set_last_refresh_time(time());
349
				$_SESSION['_keycloak_auth'] = $keycloak;
350
351
				if (isset($_SESSION['_keycloak_auth'])) {
352
					$email = appendDefaultDomain($_SESSION['_keycloak_auth']->access_token->get_claims('email'));
353
					$token = $_SESSION['_keycloak_auth']->access_token->get_payload();
354
355
					// Check if a session is already running and if the credentials match
356
					$encryptionStore = EncryptionStore::getInstance();
357
					$username = $encryptionStore->get('username');
358
					$password = $encryptionStore->get('password');
359
360
					if (!is_null($username) && !is_null($password)) {
361
						if ($username != $email || $password != $token) {
362
							WebAppAuthentication::$_errorCode = MAPI_E_INVALID_WORKSTATION_ACCOUNT;
363
							WebAppAuthentication::$_phpSession->destroy();
364
365
							return WebAppAuthentication::getErrorCode();
366
						}
367
					}
368
					else {
369
						// If no session is currently running, then store a fingerprint of the requester
370
						// in the session.
371
						$_SESSION['fingerprint'] = BrowserFingerprint::getFingerprint();
372
					}
373
					// Give the session a new id
374
					session_regenerate_id();
375
376
					WebAppAuthentication::login($email, $token);
377
					// Store the credentials in the session if logging in was successful
378
					if (WebAppAuthentication::$_errorCode === NOERROR) {
379
						WebAppAuthentication::_storeCredentialsInSession($email, $token);
380
381
						return WebAppAuthentication::getErrorCode();
382
					}
383
				}
384
			}
385
			header('Location:' . $keycloak->login_url($keycloak->redirect_url) . '');
386
		}
387
388
		return WebAppAuthentication::getErrorCode();
389
	}
390
391
	/**
392
	 * Logs the user in with a given username and token in $_POST and logs
393
	 * in with the special flag for token authentication enabled. If $new
394
	 * is true it's assumed that a session does not exists and there will
395
	 * be a new one generated and fingerprint stored in session which is
396
	 * later compared after logon. After successful logon the session is stored.
397
	 *
398
	 * @param bool $new true if user has no session yet
399
	 *
400
	 * @return int|void
401
	 */
402
	public static function authenticateWithToken($new = true) {
403
		if (empty($_POST['token'])) {
404
			WebAppAuthentication::$_errorCode = MAPI_E_LOGON_FAILED;
405
406
			return WebAppAuthentication::getErrorCode();
407
		}
408
409
		if ($new) {
410
			// If no session is currently running, then store a fingerprint of the requester
411
			// in the session.
412
			$_SESSION['fingerprint'] = BrowserFingerprint::getFingerprint();
413
414
			// Give the session a new id
415
			session_regenerate_id();
416
		}
417
418
		WebAppAuthentication::$_errorCode = WebAppAuthentication::getMAPISession()->logon(
419
			$_POST['username'],
420
			$_POST['token'],
421
			DEFAULT_SERVER,
422
			null,
423
			null,
424
			0
425
		);
426
427
		// Store the credentials in the session if logging in was successful
428
		if (WebAppAuthentication::$_errorCode === NOERROR) {
429
			WebAppAuthentication::_storeCredentialsInSession($_POST['username'], $_POST['token']);
430
			WebAppAuthentication::_storeMAPISession(WebAppAuthentication::$_mapiSession->getSession());
431
		}
432
433
		return WebAppAuthentication::getErrorCode();
434
	}
435
436
	/**
437
	 * Tries to authenticate the user with credentials from the session. When credentials
438
	 * are found in the session it will return the error code from the logon attempt with
439
	 * those credentials, otherwise it will return void.
440
	 *
441
	 * Before trying to logon, it will compare the requesters fingerprint with the
442
	 * fingerprint stored in the session. If they are not the same, the session will be
443
	 * destroyed and the script will be killed.
444
	 *
445
	 * @return int|void
446
	 */
447
	private static function _authenticateWithSession() {
448
		// Check if the session hasn't timed out
449
		if (WebAppAuthentication::$_phpSession->hasTimedOut()) {
450
			// Using a MAPI error code here, while it is not really a MAPI session timeout
451
			// However to the user this should make no difference, so the MAPI error will do.
452
			WebAppAuthentication::$_errorCode = MAPI_E_END_OF_SESSION;
453
454
			return WebAppAuthentication::getErrorCode();
455
		}
456
457
		// Now check if we stored credentials in the session (in the encryption store)
458
		$encryptionStore = EncryptionStore::getInstance();
459
		$username = $encryptionStore->get('username');
460
		$password = $encryptionStore->get('password');
461
		if (is_null($username) || is_null($password)) {
462
			return;
463
		}
464
465
		// Check if the browser fingerprint is the same as that of the browser that was
466
		// used to login in the first place.
467
		if ($_SESSION['fingerprint'] !== BrowserFingerprint::getFingerprint()) {
468
			// Something bad has happened. This must be someone who stole a session cookie!!!
469
			// We will delete the session and stop the script without any error message
470
			WebAppAuthentication::$_phpSession->destroy();
471
472
			exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
473
		}
474
475
		return WebAppAuthentication::login($username, $password);
476
	}
477
478
	/**
479
	 * Returns the username that is stored in the session.
480
	 *
481
	 * @return string
482
	 */
483
	public static function getUserName() {
484
		$encryptionStore = EncryptionStore::getInstance();
485
486
		return $encryptionStore->get('username');
487
	}
488
}
489
490
// Instantiate the class
491
WebAppAuthentication::getInstance();
492