Something went wrong on our end
-
Jean-Laurent DUZANT authoredJean-Laurent DUZANT authored
AuthenticationController.php 25.08 KiB
<?php
/**
* Copyright Maarch since 2008 under license.
* See LICENSE.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 History\controllers\HistoryController;
use Respect\Validation\Validator;
use Slim\Psr7\Request;
use SrcCore\http\Response;
use SrcCore\models\AuthenticationModel;
use SrcCore\models\CoreConfigModel;
use SrcCore\models\PasswordModel;
use SrcCore\models\ValidatorModel;
use User\controllers\UserController;
use User\models\UserModel;
class AuthenticationController
{
const MAX_DURATION_TOKEN = 30; //Minutes
const ROUTES_WITHOUT_AUTHENTICATION = [
'GET/authenticationInformations', 'POST/authenticate', 'GET/authenticate/token',
'POST/password', 'PUT/password', 'GET/passwordRules', 'GET/languages/{lang}', 'GET/commitInformation',
'GET/documents/{id}/workflows/{workflowId}/files/{fileId}', 'GET/externalSignatoryBookReturn'
];
public function getInformations(Request $request, Response $response)
{
$connection = ConfigurationModel::getConnection();
$encryptKeyChanged = CoreConfigModel::hasEncryptKeyChanged();
$path = CoreConfigModel::getConfigPath();
$coreUrl = UrlController::getCoreUrl();
$hashedPath = md5($path);
$authUri = null;
$emailConfiguration = ConfigurationModel::getByIdentifier(['identifier' => 'emailServer', 'select' => ['value']]);
$emailConfiguration = !empty($emailConfiguration[0]['value']) ? json_decode($emailConfiguration[0]['value'], true) : null;
$mailServerOnline = !empty($emailConfiguration['online']);
$customization = ConfigurationModel::getByIdentifier(['identifier' => 'customization', 'select' => ['value']]);
$customization = !empty($customization[0]['value']) ? json_decode($customization[0]['value'], true) : null;
if($connection == 'cas') {
$casConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'casServer', 'select' => ['value']]);
if (empty($casConfigurations[0])) {
return $response->withStatus(400)->withJson(['errors' => 'Cas configuration is empty']);
}
$casConfigurations = $casConfigurations[0];
$casConfigurations = json_decode($casConfigurations['value'] ?? [], true);
$hostname = (string)$casConfigurations['url'] ?? '';
$port = (string)$casConfigurations['port'] ?? '';
$uri = (string)$casConfigurations['context'] ?? '';
$authUri = "https://{$hostname}:{$port}{$uri}/?service=" . $coreUrl . 'dist/';
}
return $response->withJson([
'connection' => $connection,
'changeKey' => !$encryptKeyChanged,
'instanceId' => $hashedPath,
'coreUrl' => $coreUrl,
'mailServerOnline' => $mailServerOnline,
'loginMessage' => $customization['loginMessage'] ?? null,
'applicationUrl' => $customization['applicationUrl'] ?? null,
'authUri' => $authUri
]);
}
public static function authentication($authorizationHeaders = [])
{
$id = null;
if (!empty($_SERVER['PHP_AUTH_USER']) && !empty($_SERVER['PHP_AUTH_PW'])) {
$login = strtolower($_SERVER['PHP_AUTH_USER']);
if (AuthenticationModel::authentication(['login' => $login, 'password' => $_SERVER['PHP_AUTH_PW']])) {
$user = UserModel::getByLogin(['select' => ['id'], 'login' => $login]);
$id = $user['id'];
}
} else {
if (!empty($authorizationHeaders)) {
$token = null;
foreach ($authorizationHeaders as $authorizationHeader) {
if (strpos($authorizationHeader, 'Bearer') === 0) {
$token = str_replace('Bearer ', '', $authorizationHeader);
}
}
if (!empty($token)) {
try {
$jwt = AuthenticationModel::decodeToken($token, CoreConfigModel::getEncryptKey());
} catch (\Exception $e) {
return null;
}
$jwt['user'] = (array)$jwt['user'];
if (!empty($jwt) && !empty($jwt['user']['id'])) {
$id = $jwt['user']['id'];
}
}
}
}
return $id;
}
public function authenticate(Request $request, Response $response)
{
$body = $request->getParsedBody();
$connection = ConfigurationModel::getConnection();
if (in_array($connection, ['default', 'ldap'])) {
if (!array_key_exists('login', $body) || !array_key_exists('password', $body) || !Validator::stringType()->notEmpty()->validate($body['login']) || !Validator::stringType()->notEmpty()->validate($body['password'])) {
return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
}
}
$login = strtolower($body['login']);
if ($connection == 'ldap') {
$ldapConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'ldapServer', 'select' => ['value']]);
if (empty($ldapConfigurations)) {
return $response->withStatus(400)->withJson(['errors' => 'Ldap configuration is missing']);
}
foreach ($ldapConfigurations as $ldapConfiguration) {
$ldapConfiguration = json_decode($ldapConfiguration['value'], true);
$uri = ($ldapConfiguration['ssl'] === true ? "LDAPS://{$ldapConfiguration['uri']}" : $ldapConfiguration['uri']);
$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($ldapConfiguration['prefix']) ? $ldapConfiguration['prefix'] . '\\' . $body['login'] : $body['login']);
$ldapLogin = (!empty($ldapConfiguration['suffix']) ? $ldapLogin . $ldapConfiguration['suffix'] : $ldapLogin);
if (!empty($ldapConfiguration['baseDN'])) { //OpenLDAP
$search = @ldap_search($ldap, $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);
if ($entries === false) {
$error = 'Ldap fetching failed : ' . ldap_error($ldap);
continue;
}
if ($entries['count'] < 1) {
$error = 'No entries found in ldap search : invalid user DN or ldap configuration';
continue;
}
$ldapLogin = $entries[0]['dn'];
}
$authenticated = @ldap_bind($ldap, $ldapLogin, $body['password']);
if (!$authenticated) {
$error = ldap_error($ldap);
}
}
if (empty($authenticated) && !empty($error) && $error != 'Invalid credentials') {
return $response->withStatus(400)->withJson(['errors' => $error]);
}
} elseif ($connection == 'x509') {
if (!empty($_SERVER['SSL_CLIENT_CERT'])) {
$x509Fingerprint = openssl_x509_fingerprint($_SERVER["SSL_CLIENT_CERT"]);
$user = UserModel::get(['select' => ['login'], 'where' => ['x509_fingerprint = ?'], 'data' => [strtoupper(wordwrap($x509Fingerprint, 2, " ", true))]]);
if (empty($user)) {
return $response->withStatus(401)->withJson(['errors' => 'Authentication unauthorized']);
}
$login = $user[0]['login'];
} else {
return $response->withStatus(401)->withJson(['errors' => 'No certificate detected']);
}
$authenticated = true;
} elseif ($connection == 'kerberos') {
if (!empty($_SERVER['REMOTE_USER']) && $_SERVER['AUTH_TYPE'] == 'Negotiate') {
$login = strtolower($_SERVER['REMOTE_USER']);
} else {
return $response->withStatus(401)->withJson(['errors' => 'No identifier detected for kerberos']);
}
$authenticated = true;
} else if ($connection == 'azure_saml') {
$authenticated = AuthenticationController::azureSamlConnection();
if (!empty($authenticated['errors'])) {
return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]);
}
$login = strtolower($authenticated['login']);
$authenticated = true;
} elseif ($connection == 'cas') {
$authenticated = AuthenticationController::casConnection();
$login = $authenticated['login'];
if (!empty($authenticated['errors'])) {
return $response->withStatus(401)->withJson(['errors' => $authenticated['errors']]);
}
} else {
$authenticated = AuthenticationModel::authentication(['login' => $login, 'password' => $body['password']]);
}
if (empty($authenticated)) {
$user = UserModel::getByLogin(['login' => $login, 'select' => ['id']]);
if (!empty($user)) {
$handle = AuthenticationController::handleFailedAuthentication(['userId' => $user['id']]);
if (!empty($handle['accountLocked'])) {
return $response->withStatus(401)->withJson(['errors' => 'Account Locked', 'date' => $handle['lockedDate']]);
}
}
return $response->withStatus(401)->withJson(['errors' => 'Authentication Failed']);
}
$user = UserModel::getByLogin(['login' => $login, 'select' => ['id', '"isRest"', 'refresh_token']]);
if (empty($user) || $user['isRest']) {
return $response->withStatus(403)->withJson(['errors' => 'Authentication unauthorized']);
}
$GLOBALS['id'] = $user['id'];
$user['refresh_token'] = json_decode($user['refresh_token'], true);
foreach ($user['refresh_token'] as $key => $refreshToken) {
try {
AuthenticationModel::decodeToken($refreshToken, CoreConfigModel::getEncryptKey());
} 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'])],
'where' => ['id = ?'],
'data' => [$user['id']]
]);
$response = $response->withHeader('Token', AuthenticationController::getJWT());
$response = $response->withHeader('Refresh-Token', $refreshToken);
HistoryController::add([
'code' => 'OK',
'objectType' => 'users',
'objectId' => $user['id'],
'type' => 'LOGIN',
'message' => '{userLogIn}'
]);
return $response->withStatus(204);
}
private static function casConnection()
{
$casConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'casServer', 'select' => ['value']]);
if (empty($casConfigurations[0])) {
return ['errors' => 'Cas authentication failed'];
}
$casConfigurations = $casConfigurations[0];
$casConfigurations = json_decode($casConfigurations['value'], true);
$version = (string)$casConfigurations['version'];
$hostname = (string)$casConfigurations['url'];
$port = (string)$casConfigurations['port'];
$uri = (string)$casConfigurations['context'];
$certificate = (string)$casConfigurations['certificate'];
$separator = (string)$casConfigurations['separator'];
if (!in_array($version, ['CAS_VERSION_2_0', 'CAS_VERSION_3_0'])) {
return ['errors' => 'Cas version not supported'];
}
\phpCAS::client(constant($version), $hostname, (int)$port, $uri, $version != 'CAS_VERSION_3_0');
if (!empty($certificate)) {
\phpCAS::setCasServerCACert($certificate);
} else {
\phpCAS::setNoCasServerValidation();
}
\phpCAS::setFixedServiceURL(UrlController::getCoreUrl() . 'dist/');
\phpCAS::setNoClearTicketsFromUrl();
if (!\phpCAS::isAuthenticated()) {
return ['errors' => 'Cas authentication failed'];
}
$casId = \phpCAS::getUser();
if (!empty($separator)) {
$login = explode($separator, $casId)[0];
} else {
$login = $casId;
}
return ['login' => $login];
}
private static function casDisconnection()
{
$casConfigurations = ConfigurationModel::getByIdentifier(['identifier' => 'casServer', 'select' => ['value']]);
if (empty($casConfigurations)) {
return ['errors' => 'Cas authentication failed'];
}
$casConfigurations = $casConfigurations[0];
$casConfigurations = json_decode($casConfigurations['value'], true);
$version = (string)$casConfigurations['version'];
$hostname = (string)$casConfigurations['url'];
$port = (string)$casConfigurations['port'];
$uri = (string)$casConfigurations['context'];
$certificate = (string)$casConfigurations['certificate'];
\phpCAS::setVerbose(true);
\phpCAS::client(constant($version), $hostname, (int)$port, $uri, $version != 'CAS_VERSION_3_0');
if (!empty($certificate)) {
\phpCAS::setCasServerCACert($certificate);
} else {
\phpCAS::setNoCasServerValidation();
}
\phpCAS::setFixedServiceURL(UrlController::getCoreUrl() . 'dist/');
\phpCAS::setNoClearTicketsFromUrl();
$logoutUrl = \phpCAS::getServerLogoutURL();
return ['logoutUrl' => $logoutUrl];
}
private static function azureSamlConnection()
{
$libPath = CoreConfigModel::getSimpleSamlLibrary();
if (!is_file($libPath)) {
return ['errors' => 'Library simplesamlphp not present'];
}
require_once($libPath);
$as = new \SimpleSAML\Auth\Simple('default-sp');
$as->requireAuth([
'ReturnTo' => UrlController::getCoreUrl(),
'skipRedirection' => true
]);
$attributes = $as->getAttributes();
$login = $attributes['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'][0];
if (empty($login)) {
return ['errors' => 'Authentication Failed : login not present in attributes'];
}
return ['login' => $login];
}
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 = AuthenticationModel::decodeToken($queryParams['refreshToken'], CoreConfigModel::getEncryptKey());
$jwt['user'] = (array)$jwt['user'] ?? [];
} 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 function logout(Request $request, Response $response)
{
$loggingMethod = ConfigurationModel::getConnection();
$logoutUrl = null;
if ($loggingMethod == 'cas') {
$disconnection = AuthenticationController::casDisconnection();
$logoutUrl = $disconnection['logoutUrl'];
}
HistoryController::add([
'code' => 'OK',
'objectType' => 'users',
'objectId' => $GLOBALS['id'],
'type' => 'LOGOUT',
'message' => '{userLogOut}'
]);
return $response->withJson(['logoutUrl' => $logoutUrl]);
}
public static function getJWT()
{
$sessionTime = AuthenticationController::MAX_DURATION_TOKEN;
$loadedJson = CoreConfigModel::getConfig();
if (!empty($loadedJson['config']['sessionTime'])) {
if ($sessionTime > $loadedJson['config']['sessionTime']) {
$sessionTime = $loadedJson['config']['sessionTime'];
}
}
$user = UserController::getUserInformationsById(['id' => $GLOBALS['id']]);
unset($user['picture']);
$token = [
'exp' => time() + 60 * $sessionTime,
'user' => $user,
'connection' => ConfigurationModel::getConnection()
];
return AuthenticationModel::generateToken($token, CoreConfigModel::getEncryptKey());
}
public static function getRefreshJWT()
{
$sessionTime = AuthenticationController::MAX_DURATION_TOKEN;
$loadedJson = CoreConfigModel::getConfig();
if (!empty($loadedJson['config']['sessionTime'])) {
$sessionTime = $loadedJson['config']['sessionTime'];
}
$token = [
'exp' => time() + 60 * $sessionTime,
'user' => [
'id' => $GLOBALS['id']
]
];
return AuthenticationModel::generateToken($token, CoreConfigModel::getEncryptKey());
}
public static function getResetJWT($args = [])
{
$token = [
'exp' => time() + $args['expirationTime'],
'user' => [
'id' => $args['id']
],
'connection' => ConfigurationModel::getConnection()
];
return AuthenticationModel::generateToken($token, CoreConfigModel::getEncryptKey());
}
public static function sendAccountActivationNotification(array $args)
{
$connection = ConfigurationModel::getConnection();
if ($connection == 'default') {
$resetToken = AuthenticationController::getResetJWT(['id' => $args['userId'], 'expirationTime' => 1209600]); // 14 days
UserModel::update(['set' => ['reset_token' => $resetToken], 'where' => ['id = ?'], 'data' => [$args['userId']]]);
$user = UserModel::getById(['select' => ['login'], 'id' => $args['userId']]);
$lang = LanguageController::get(['lang' => 'fr']);
$url = ConfigurationModel::getApplicationUrl() . 'dist/#/update-password?token=' . $resetToken;
EmailController::createEmail([
'userId' => $args['userId'],
'data' => [
'sender' => 'Notification',
'recipients' => [$args['userEmail']],
'subject' => $lang['notificationNewAccountSubject'],
'body' => $lang['notificationNewAccountBody'] . '<a href="' . $url . '">'.$url.'</a>' . $lang['notificationNewAccountId'] . ' ' . $user['login'] . $lang['notificationNewAccountFooter'],
'isHtml' => true
]
]);
}
return true;
}
public static function isRouteAvailable(array $args)
{
ValidatorModel::notEmpty($args, ['userId', 'currentRoute']);
ValidatorModel::intVal($args, ['userId']);
ValidatorModel::stringType($args, ['currentRoute']);
$user = UserModel::getById(['select' => ['password_modification_date', '"isRest"'], 'id' => $args['userId']]);
if (!in_array($args['currentRoute'], ['/passwordRules', '/users/{id}/password']) && empty($user['isRest'])) {
$connectionConfiguration = ConfigurationModel::getByIdentifier(['identifier' => 'connection', 'select' => ['value']]);
if ($connectionConfiguration[0]['value'] == '"default"') {
$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' => 'Password expired : 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']]);
$set = [];
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([
'set' => $set,
'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 getGitRepoInformation(Request $request, Response $response)
{
if (!file_exists('package.json')) {
return $response->withJson(['url' => null]);
}
$license = json_decode(file_get_contents('package.json'), true);
if (empty($license) || empty($license['license'])) {
return $response->withJson(['url' => null]);
}
if ($license['license'] == "GPL-3.0") {
return $response->withJson(['url' => "https://labs.maarch.org/maarch/MaarchParapheur"]);
} elseif (strtolower($license['license']) == "maarch") {
return $response->withJson(['url' => "https://labs.maarch.org/maarch/MaarchParapheurPro"]);
} else {
return $response->withJson(['url' => null]);
}
}
public function getGitCommitInformation(Request $request, Response $response)
{
if (!file_exists('.git/HEAD')) {
return $response->withJson(['hash' => null]);
}
$head = file_get_contents('.git/HEAD');
if ($head === false) {
return $response->withJson(['hash' => null]);
}
preg_match('#^ref:(.+)$#', $head, $matches);
$currentHead = trim($matches[1]);
if (empty($currentHead)) {
return $response->withJson(['hash' => null]);
}
$hash = file_get_contents('.git/' . $currentHead);
if ($hash === false) {
return $response->withJson(['hash' => null]);
}
$hash = explode("\n", $hash)[0];
return $response->withJson(['hash' => $hash]);
}
}