Newer
Older
<?php
/**
* Copyright Maarch since 2008 under licence GPLv3.
* See LICENCE.txt file at the root folder for more details.
* This file is part of Maarch software.
*/
/**
* @brief Authentication Controller
*
* @author dev@maarch.org
*/
namespace SrcCore\controllers;
use Configuration\models\ConfigurationModel;
use Email\controllers\EmailController;
use Firebase\JWT\JWT;
use History\controllers\HistoryController;
use Respect\Validation\Validator;
use Slim\Http\Request;
use Slim\Http\Response;
use SrcCore\models\CoreConfigModel;
use SrcCore\models\PasswordModel;
use SrcCore\models\ValidatorModel;
use User\models\UserModel;
class AuthenticationController
{
const MAX_DURATION_TOKEN = 30; //Minutes
const ROUTES_WITHOUT_AUTHENTICATION = [
'GET/authenticationInformations', 'PUT/versionsUpdateSQL', 'GET/validUrl', 'GET/authenticate/token', 'GET/images', 'POST/password', 'PUT/password', 'GET/passwordRules',
'GET/jnlp/{jnlpUniqueId}', 'GET/onlyOffice/mergedFile', 'POST/onlyOfficeCallback', 'POST/authenticate',
'GET/installer/prerequisites', 'GET/installer/databaseConnection', 'GET/installer/sqlDataFiles', 'GET/installer/docservers', 'GET/installer/custom',
'POST/installer/custom', 'POST/installer/database', 'POST/installer/docservers', 'POST/installer/customization',
'PUT/installer/administrator', 'DELETE/installer/lock',
'GET/wopi/files/{id}', 'GET/wopi/files/{id}/contents', 'POST/wopi/files/{id}/contents','GET/onlyOffice/content','GET/languages/{lang}',
public function getInformations(Request $request, Response $response)
{
$path = CoreConfigModel::getConfigPath();
$hashedPath = md5($path);
$appName = CoreConfigModel::getApplicationName();
$parameter = ParameterModel::getById(['id' => 'loginpage_message', 'select' => ['param_value_string']]);
Guillaume Heurtier
committed
$encryptKey = CoreConfigModel::getEncryptKey();
return $response->withJson([
'instanceId' => $hashedPath,
'applicationName' => $appName,
'loginMessage' => $parameter['param_value_string'] ?? null,
'changeKey' => $encryptKey == 'Security Key Maarch Courrier #2008'
]);
public function getValidUrl(Request $request, Response $response)
{
if (!is_file('custom/custom.json')) {
return $response->withJson(['message' => 'No custom file', 'lang' => 'noConfiguration']);
}
$jsonFile = file_get_contents('custom/custom.json');
$jsonFile = json_decode($jsonFile, true);
return $response->withJson(['message' => 'No custom', 'lang' => 'noConfiguration']);
return $response->withJson(['message' => 'There is more than 1 custom', 'lang' => 'moreOneCustom']);
}
$url = null;
if (!empty($jsonFile[0]['path'])) {
$coreUrl = UrlController::getCoreUrl();
$url = $coreUrl . $jsonFile[0]['path'] . "/dist/index.html";
} elseif (!empty($jsonFile[0]['uri'])) {
$url = $jsonFile[0]['uri'] . "/dist/index.html";
}
return $response->withJson(['url' => $url]);
}
public static function authentication($authorizationHeaders = [])
{
$userId = null;
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
if (AuthenticationModel::authentication(['login' => $_SERVER['PHP_AUTH_USER'], 'password' => $_SERVER['PHP_AUTH_PW']])) {

Damien
committed
$loginMethod = CoreConfigModel::getLoggingMethod();
Guillaume Heurtier
committed
$user = UserModel::getByLogin(['select' => ['id', 'mode'], 'login' => $_SERVER['PHP_AUTH_USER']]);

Damien
committed
if ($loginMethod['id'] != 'standard') {
Guillaume Heurtier
committed
if ($user['mode'] == 'rest') {

Damien
committed
}
} else {

Damien
committed
}
if (!empty($authorizationHeaders)) {
$token = null;
foreach ($authorizationHeaders as $authorizationHeader) {
if (strpos($authorizationHeader, 'Bearer') === 0) {
$token = str_replace('Bearer ', '', $authorizationHeader);
}
}
if (!empty($token)) {
try {
$jwt = (array)JWT::decode($token, CoreConfigModel::getEncryptKey(), ['HS256']);
} catch (\Exception $e) {
return null;
}
$jwt['user'] = (array)$jwt['user'];
if (!empty($jwt) && !empty($jwt['user']['id'])) {
$userId = $jwt['user']['id'];
}
}
}
if (!empty($userId)) {
UserModel::update([
'set' => ['reset_token' => null],
'data' => [$userId]
]);
}
public static function isRouteAvailable(array $args)
ValidatorModel::notEmpty($args, ['userId', 'currentRoute', 'currentMethod']);
ValidatorModel::intVal($args, ['userId']);
ValidatorModel::stringType($args, ['currentRoute', 'currentMethod']);
$user = UserModel::getById(['select' => ['status', 'password_modification_date', 'mode', 'authorized_api'], 'id' => $args['userId']]);
Guillaume Heurtier
committed
if ($user['mode'] == 'rest') {
$authorizedApi = json_decode($user['authorized_api'], true);
if (!empty($authorizedApi) && !in_array($args['currentMethod'].$args['currentRoute'], $authorizedApi)) {
return ['isRouteAvailable' => false, 'errors' => 'This route is not authorized for this user'];
}
return ['isRouteAvailable' => true];
} elseif ($user['status'] == 'ABS' && !in_array($args['currentRoute'], ['/users/{id}/status', '/currentUser/profile', '/header', '/passwordRules', '/users/{id}/password'])) {
return ['isRouteAvailable' => false, 'errors' => 'User is ABS and must be activated'];
}
if (!in_array($args['currentRoute'], ['/passwordRules', '/users/{id}/password'])) {
$loggingMethod = CoreConfigModel::getLoggingMethod();
if (!in_array($loggingMethod['id'], ['sso', 'cas', 'ldap', 'keycloak', 'shibboleth'])) {
$passwordRules = PasswordModel::getEnabledRules();
if (!empty($passwordRules['renewal'])) {
$currentDate = new \DateTime();
$lastModificationDate = new \DateTime($user['password_modification_date']);
$lastModificationDate->add(new \DateInterval("P{$passwordRules['renewal']}D"));
if ($currentDate > $lastModificationDate) {
return ['isRouteAvailable' => false, 'errors' => 'User must change his password'];
}
}
}
}
return ['isRouteAvailable' => true];
}
public static function handleFailedAuthentication(array $args)
ValidatorModel::notEmpty($args, ['userId']);
ValidatorModel::intVal($args, ['userId']);
$passwordRules = PasswordModel::getEnabledRules();
if (!empty($passwordRules['lockAttempts'])) {
$user = UserModel::getById(['select' => ['failed_authentication', 'locked_until'], 'id' => $args['userId']]);
if (!empty($user['locked_until'])) {
$currentDate = new \DateTime();
$lockedUntil = new \DateTime($user['locked_until']);
if ($lockedUntil < $currentDate) {
$set['locked_until'] = null;
$user['failed_authentication'] = 0;
} else {
return ['accountLocked' => true, 'lockedDate' => $user['locked_until']];
}
$set['failed_authentication'] = $user['failed_authentication'] + 1;
UserModel::update([
'where' => ['id = ?'],
'data' => [$args['userId']]
]);
if (!empty($user['failed_authentication']) && ($user['failed_authentication'] + 1) >= $passwordRules['lockAttempts'] && !empty($passwordRules['lockTime'])) {
$lockedUntil = time() + 60 * $passwordRules['lockTime'];
UserModel::update([
'set' => ['locked_until' => date('Y-m-d H:i:s', $lockedUntil)],
'where' => ['id = ?'],
'data' => [$args['userId']]
]);
return ['accountLocked' => true, 'lockedDate' => date('Y-m-d H:i:s', $lockedUntil)];
return true;
public function authenticate(Request $request, Response $response)
{
$body = $request->getParsedBody();
$check = Validator::stringType()->notEmpty()->validate($body['login']);
$check = $check && Validator::stringType()->notEmpty()->validate($body['password']);
if (!$check) {
return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
}
$login = strtolower($body['login']);
$user = UserModel::getByLogin(['login' => $login, 'select' => ['id', 'mode', 'refresh_token', 'user_id', 'status']]);
if (empty($user) || $user['mode'] == 'rest' || $user['status'] == 'SPD') {
return $response->withStatus(403)->withJson(['errors' => 'Authentication unauthorized']);
$loggingMethod = CoreConfigModel::getLoggingMethod();
if ($loggingMethod['id'] == 'standard') {
$authenticated = AuthenticationController::standardConnection(['login' => $login, 'password' => $body['password']]);
if (!empty($authenticated['date'])) {
return $response->withStatus(401)->withJson(['errors' => $authenticated['errors'], 'date' => $authenticated['date']]);
} elseif (!empty($authenticated['errors'])) {
return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]);
}
} elseif ($loggingMethod['id'] == 'ldap') {
$authenticated = AuthenticationController::ldapConnection(['login' => $login, 'password' => $body['password']]);
if (!empty($authenticated['errors'])) {
return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]);
}
}
$GLOBALS['id'] = $user['id'];
$user['refresh_token'] = json_decode($user['refresh_token'], true);
foreach ($user['refresh_token'] as $key => $refreshToken) {
try {
JWT::decode($refreshToken, CoreConfigModel::getEncryptKey(), ['HS256']);
} catch (\Exception $e) {
unset($user['refresh_token'][$key]);
}
}
$user['refresh_token'] = array_values($user['refresh_token']);
if (count($user['refresh_token']) > 10) {
array_shift($user['refresh_token']);
}
$refreshToken = AuthenticationController::getRefreshJWT();
$user['refresh_token'][] = $refreshToken;
UserModel::update([
'set' => ['reset_token' => null, 'refresh_token' => json_encode($user['refresh_token']), 'failed_authentication' => 0, 'locked_until' => null],
'where' => ['id = ?'],
'data' => [$user['id']]
]);
$response = $response->withHeader('Token', AuthenticationController::getJWT());
$response = $response->withHeader('Refresh-Token', $refreshToken);
'tableName' => 'users',
'recordId' => $user['id'],
'eventType' => 'LOGIN',
'info' => _LOGIN . ' : ' . $login,
'moduleId' => 'authentication',
'eventId' => 'login'
return $response->withStatus(204);
}
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
private static function standardConnection(array $args)
{
$login = $args['login'];
$password = $args['password'];
$authenticated = AuthenticationModel::authentication(['login' => $login, 'password' => $password]);
if (empty($authenticated)) {
$user = UserModel::getByLogin(['login' => $login, 'select' => ['id']]);
$handle = AuthenticationController::handleFailedAuthentication(['userId' => $user['id']]);
if (!empty($handle['accountLocked'])) {
return ['errors' => 'Account Locked', 'date' => $handle['lockedDate']];
}
return ['errors' => 'Authentication Failed'];
}
return true;
}
private static function ldapConnection(array $args)
{
$login = $args['login'];
$password = $args['password'];
$ldapConfigurations = CoreConfigModel::getXmlLoaded(['path' => 'modules/ldap/xml/config.xml']);
if (empty($ldapConfigurations)) {
return ['errors' => 'No ldap configurations'];
}
foreach ($ldapConfigurations->config->ldap as $ldapConfiguration) {
$ssl = (string)$ldapConfiguration->ssl;
$domain = (string)$ldapConfiguration->domain;
$prefix = (string)$ldapConfiguration->prefix_login;
$suffix = (string)$ldapConfiguration->suffix_login;
$standardConnect = (string)$ldapConfiguration->standardConnect;
$uri = ($ssl == 'true' ? "LDAPS://{$domain}" : $domain);
$ldap = @ldap_connect($uri);
if ($ldap === false) {
$error = 'Ldap connect failed : uri is maybe wrong';
continue;
}
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
ldap_set_option($ldap, LDAP_OPT_NETWORK_TIMEOUT, 10);
$ldapLogin = (!empty($prefix) ? $prefix . '\\' . $login : $login);
$ldapLogin = (!empty($suffix) ? $ldapLogin . $suffix : $ldapLogin);
if (!empty((string)$ldapConfiguration->baseDN)) { //OpenLDAP
$search = @ldap_search($ldap, (string)$ldapConfiguration->baseDN, "(uid={$ldapLogin})", ['dn']);
if ($search === false) {
$error = 'Ldap search failed : baseDN is maybe wrong => ' . ldap_error($ldap);
continue;
}
$entries = ldap_get_entries($ldap, $search);
$ldapLogin = $entries[0]['dn'];
}
$authenticated = @ldap_bind($ldap, $ldapLogin, $password);
if ($authenticated) {
break;
}
$error = ldap_error($ldap);
}
if (!empty($standardConnect) && $standardConnect == 'true') {
if (empty($authenticated)) {
$authenticated = AuthenticationModel::authentication(['login' => $login, 'password' => $password]);
} else {
$user = UserModel::getByLogin(['login' => $login, 'select' => ['id']]);
UserModel::updatePassword(['id' => $user['id'], 'password' => $password]);
}
}
if (empty($authenticated) && !empty($error) && $error != 'Invalid credentials') {
return ['errors' => $error];
} elseif (empty($authenticated) && !empty($error) && $error == 'Invalid credentials') {
return ['errors' => 'Authentication Failed'];
}
return true;
}
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
public function getRefreshedToken(Request $request, Response $response)
{
$queryParams = $request->getQueryParams();
if (!Validator::stringType()->notEmpty()->validate($queryParams['refreshToken'])) {
return $response->withStatus(400)->withJson(['errors' => 'Refresh Token is empty']);
}
try {
$jwt = JWT::decode($queryParams['refreshToken'], CoreConfigModel::getEncryptKey(), ['HS256']);
} catch (\Exception $e) {
return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
}
$user = UserModel::getById(['select' => ['id', 'refresh_token'], 'id' => $jwt->user->id]);
if (empty($user['refresh_token'])) {
return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
}
$user['refresh_token'] = json_decode($user['refresh_token'], true);
if (!in_array($queryParams['refreshToken'], $user['refresh_token'])) {
return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
}
$GLOBALS['id'] = $user['id'];
return $response->withJson(['token' => AuthenticationController::getJWT()]);
}
public static function getJWT()
{
$sessionTime = AuthenticationController::MAX_DURATION_TOKEN;
$file = CoreConfigModel::getJsonLoaded(['path' => 'apps/maarch_entreprise/xml/config.json']);
if ($file) {
if (!empty($file['config']['cookieTime'])) {
if ($sessionTime > (int)$file['config']['cookieTime']) {
$sessionTime = (int)$file['config']['cookieTime'];
$user = UserModel::getById(['id' => $GLOBALS['id'], 'select' => ['id', 'firstname', 'lastname', 'status', 'user_id as login']]);
$token = [
'exp' => time() + 60 * $sessionTime,
];
$jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());
return $jwt;
}
public static function getRefreshJWT()
{
$sessionTime = AuthenticationController::MAX_DURATION_TOKEN;
$file = CoreConfigModel::getJsonLoaded(['path' => 'apps/maarch_entreprise/xml/config.json']);
if ($file) {
$sessionTime = (int)$file['config']['cookieTime'];
}
$token = [
'exp' => time() + 60 * $sessionTime,
'user' => [
'id' => $GLOBALS['id']
]
];
$jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());
return $jwt;
}
public static function getResetJWT($args = [])
'exp' => time() + $args['expirationTime'],
'id' => $args['id']
]
];
$jwt = JWT::encode($token, CoreConfigModel::getEncryptKey());
return $jwt;
}
public static function sendAccountActivationNotification(array $args)
{
$resetToken = AuthenticationController::getResetJWT(['id' => $args['userId'], 'expirationTime' => 1209600]); // 14 days
UserModel::update(['set' => ['reset_token' => $resetToken], 'where' => ['id = ?'], 'data' => [$args['userId']]]);
$url = UrlController::getCoreUrl() . 'dist/index.html#/reset-password?token=' . $resetToken;
Guillaume Heurtier
committed
$configuration = ConfigurationModel::getByPrivilege(['privilege' => 'admin_email_server', 'select' => ['value']]);
$configuration = json_decode($configuration['value'], true);
if (!empty($configuration['from'])) {
$sender = $configuration['from'];
} else {
$sender = $args['userEmail'];
}
EmailController::createEmail([
'userId' => $args['userId'],
'data' => [
'sender' => ['email' => $sender],
'recipients' => [$args['userEmail']],
'object' => _NOTIFICATIONS_USER_CREATION_SUBJECT,
'body' => _NOTIFICATIONS_USER_CREATION_BODY . '<a href="' . $url . '">'._CLICK_HERE.'</a>' . _NOTIFICATIONS_USER_CREATION_FOOTER,
'isHtml' => true,
'status' => 'WAITING'
]
]);
return true;
}