Skip to content

Commit

Permalink
Merge pull request #72 from monarc-project/feature/captcha
Browse files Browse the repository at this point in the history
Captcha
  • Loading branch information
ruslanbaidan authored Feb 6, 2025
2 parents c0305a5 + 1eb8571 commit 681856c
Show file tree
Hide file tree
Showing 9 changed files with 292 additions and 37 deletions.
1 change: 1 addition & 0 deletions config/module.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@
DeprecatedTable\GuideItemTable::class => AutowireFactory::class,
DeprecatedTable\HistoricalTable::class => AutowireFactory::class,
DeprecatedTable\DeliveriesModelsTable::class => AutowireFactory::class,
Table\ActionHistoryTable::class => Table\Factory\ClientEntityManagerFactory::class,
Table\AnrInstanceMetadataFieldTable::class => Table\Factory\CoreEntityManagerFactory::class,
Table\AnrTable::class => Table\Factory\CoreEntityManagerFactory::class,
Table\AmvTable::class => Table\Factory\CoreEntityManagerFactory::class,
Expand Down
83 changes: 60 additions & 23 deletions src/Adapter/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
namespace Monarc\Core\Adapter;

use Doctrine\ORM\EntityNotFoundException;
use Monarc\Core\Entity\ActionHistorySuperClass;
use Monarc\Core\Entity\UserSuperClass;
use Monarc\Core\Service\ActionHistoryService;
use Monarc\Core\Table\UserTable;
use Monarc\Core\Service\ConfigService;
use Laminas\Authentication\Adapter\AbstractAdapter;
Expand All @@ -24,19 +26,14 @@ class Authentication extends AbstractAdapter
public const TWO_FA_TO_SET_UP = 3;
public const TWO_FA_FAILED = 4;

/** @var UserTable */
private $userTable;

/** @var UserSuperClass */
protected $user;

/** @var ConfigService */
private $configService;

public function __construct(UserTable $userTable, ConfigService $configService)
{
$this->userTable = $userTable;
$this->configService = $configService;
public function __construct(
private UserTable $userTable,
private ConfigService $configService,
private ActionHistoryService $actionHistoryService
) {
}

/**
Expand Down Expand Up @@ -70,16 +67,31 @@ public function authenticate(string $token = ''): Result
$credential = $this->getCredential();
try {
$user = $this->userTable->findByEmail($identity);
} catch (EntityNotFoundException $e) {
return new Result(Result::FAILURE_IDENTITY_NOT_FOUND, $this->getIdentity());
} catch (EntityNotFoundException) {
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'email is not found',
], ActionHistorySuperClass::STATUS_FAILURE);

return new Result(Result::FAILURE_IDENTITY_NOT_FOUND, $identity);
}

if (!$user->isActive()) {
return new Result(Result::FAILURE_IDENTITY_NOT_FOUND, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'user is inactive',
], ActionHistorySuperClass::STATUS_FAILURE, $user);

return new Result(Result::FAILURE_IDENTITY_NOT_FOUND, $identity);
}

if (!password_verify($credential, $user->getPassword())) {
return new Result(Result::FAILURE_CREDENTIAL_INVALID, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'password is incorrect',
], ActionHistorySuperClass::STATUS_FAILURE, $user);

return new Result(Result::FAILURE_CREDENTIAL_INVALID, $identity);
}

$this->setUser($user);
Expand All @@ -88,39 +100,54 @@ public function authenticate(string $token = ''): Result
if (!$user->isTwoFactorAuthEnabled()) {
/* Validate if 2FA is enforced on the platform. */
if (!$this->configService->isTwoFactorAuthEnforced()) {
return new Result(Result::SUCCESS, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'successfully logged in without 2FA',
], ActionHistorySuperClass::STATUS_SUCCESS, $user);

return new Result(Result::SUCCESS, $identity);
}

if (empty($token)) {
/* 2FA enforced and missing tokens in order to activate 2FA */
return new Result(static::TWO_FA_TO_SET_UP, $this->getIdentity());
return new Result(static::TWO_FA_TO_SET_UP, $identity);
}

/* 2FA enforced and received tokens in order to activate 2FA */
/* tokens are the verification code and the otp secret */
$tokens = explode(":", $token);
$tokens = explode(':', $token);
// verify the submitted OTP token
$tfa = new TwoFactorAuth('MONARC TwoFactorAuth');
if ($tokens !== false && $tfa->verifyCode($tokens[0], $tokens[1])) {
$user->setSecretKey($tokens[0]);
$user->setTwoFactorAuthEnabled(true);
$this->userTable->save($user);

return new Result(Result::SUCCESS, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'successfully logged in with 2FA initial setup',
], ActionHistorySuperClass::STATUS_SUCCESS, $user);

return new Result(Result::SUCCESS, $identity);
}

return new Result(static::TWO_FA_TO_SET_UP, $this->getIdentity());
return new Result(static::TWO_FA_TO_SET_UP, $identity);
}

/* Validate if the 2FA token has been submitted. */
if (empty($token)) {
return new Result(static::TWO_FA_REQUIRED, $this->getIdentity());
return new Result(static::TWO_FA_REQUIRED, $identity);
}

/* Verify the submitted OTP token. */
$tfa = new TwoFactorAuth('MONARC TwoFactorAuth');
if ($tfa->verifyCode($user->getSecretKey(), $token)) {
return new Result(Result::SUCCESS, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'successfully logged in with 2FA OTP code',
], ActionHistorySuperClass::STATUS_SUCCESS, $user);

return new Result(Result::SUCCESS, $identity);
}

/* Verify the submitted recovery code. */
Expand All @@ -131,10 +158,20 @@ public function authenticate(string $token = ''): Result
$user->setRecoveryCodes($recoveryCodes);
$this->userTable->save($user);

return new Result(Result::SUCCESS, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => 'successfully logged in with 2FA recovery code',
], ActionHistorySuperClass::STATUS_SUCCESS, $user);

return new Result(Result::SUCCESS, $identity);
}
}

return new Result(static::TWO_FA_FAILED, $this->getIdentity());
$this->actionHistoryService->createActionHistory(ActionHistorySuperClass::ACTION_LOGIN_ATTEMPT, [
'identity' => $identity,
'comment' => '2FA validation failed',
], ActionHistorySuperClass::STATUS_FAILURE, $user);

return new Result(static::TWO_FA_FAILED, $identity);
}
}
20 changes: 20 additions & 0 deletions src/Entity/ActionHistory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

namespace Monarc\Core\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Table(name="actions_history", indexes={
* @ORM\Index(name="action", columns={"action"}),
* })
* @ORM\Entity
*/
class ActionHistory extends ActionHistorySuperClass
{
}
118 changes: 118 additions & 0 deletions src/Entity/ActionHistorySuperClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php declare(strict_types=1);
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

namespace Monarc\Core\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Table(name="actions_history")
* @ORM\MappedSuperclass
* @ORM\HasLifecycleCallbacks()
*/
class ActionHistorySuperClass
{
use Traits\CreateEntityTrait;

public const ACTION_LOGIN_ATTEMPT = 'login_attempt';

public const STATUS_SUCCESS = 0;
public const STATUS_FAILURE = 1;

/**
* @var int
*
* @ORM\Column(name="id", type="integer", nullable=false)
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;

/**
* @var string
*
* @ORM\Column(name="action", type="string", length=100, nullable=false)
*/
protected $action;

/**
* @var string
*
* @ORM\Column(name="data", type="string", length=4096, nullable=false)
*/
protected $data = '';

/**
* @var ?UserSuperClass
*
* @ORM\ManyToOne(targetEntity="User", cascade={"persist"})
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=true)
* })
*/
protected $user;

/**
* @var int
*
* @ORM\Column(name="status", type="smallint", options={"unsigned":true, "default":0})
*/
protected $status = self::STATUS_SUCCESS;

public function getId(): ?int
{
return $this->id;
}

public function getAction(): string
{
return $this->action;
}

public function setAction(string $action): self
{
$this->action = $action;

return $this;
}

public function getData(): string
{
return $this->data;
}

public function setData(string $data): self
{
$this->data = $data;

return $this;
}

public function getUser(): ?UserSuperClass
{
return $this->user;
}

public function setUser(?UserSuperClass $user): self
{
$this->user = $user;

return $this;
}

public function getStatus(): int
{
return $this->status;
}

public function setStatus(int $status): self
{
$this->status = $status;

return $this;
}
}
1 change: 0 additions & 1 deletion src/Entity/UserSuperClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
use Monarc\Core\Entity\Traits;

/**
* @ORM\Table(name="users")
Expand Down
48 changes: 48 additions & 0 deletions src/Service/ActionHistoryService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php declare(strict_types=1);
/**
* @link https://github.com/monarc-project for the canonical source repository
* @copyright Copyright (c) 2016-2025 Luxembourg House of Cybersecurity LHC.lu - Licensed under GNU Affero GPL v3
* @license MONARC is licensed under GNU Affero General Public License version 3
*/

namespace Monarc\Core\Service;

use Monarc\Core\Entity\ActionHistorySuperClass;
use Monarc\Core\Entity\UserSuperClass;
use Monarc\Core\Table\ActionHistoryTable;

class ActionHistoryService
{
public function __construct(private ActionHistoryTable $actionHistoryTable)
{
}

/**
* @return ActionHistorySuperClass[]
*/
public function getActionsHistoryByAction(string $action, int $limit = 0): array
{
return $this->actionHistoryTable->findByActionOrderByDate($action, $limit);
}

public function createActionHistory(
string $action,
array $data,
int $status = ActionHistorySuperClass::STATUS_SUCCESS,
?UserSuperClass $user = null,
bool $saveInDb = true
): ActionHistorySuperClass {
$actionHistoryEntityName = $this->actionHistoryTable->getEntityName();
/** @var ActionHistorySuperClass $actionHistory */
$actionHistory = new $actionHistoryEntityName();
$actionHistory
->setAction($action)
->setData(json_encode($data, JSON_THROW_ON_ERROR))
->setUser($user)
->setStatus($status)
->setCreator('System');
$this->actionHistoryTable->save($actionHistory, $saveInDb);

return $actionHistory;
}
}
16 changes: 3 additions & 13 deletions src/Service/AuthenticationService.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,13 @@ class AuthenticationService
public const TWO_FA_CODE_REQUIRED = '2FARequired';
public const TWO_FA_CODE_TO_BE_CONFIGURED = '2FAToBeConfigured';

private ConfigService $configService;

private AuthenticationStorage $authenticationStorage;

private AuthenticationAdapter $authenticationAdapter;

private TwoFactorAuth $tfa;

public function __construct(
ConfigService $configService,
AuthenticationStorage $authenticationStorage,
AuthenticationAdapter $authenticationAdapter
private ConfigService $configService,
private AuthenticationStorage $authenticationStorage,
private AuthenticationAdapter $authenticationAdapter
) {
$this->configService = $configService;
$this->authenticationStorage = $authenticationStorage;
$this->authenticationAdapter = $authenticationAdapter;
$qr = new EndroidQrCodeProvider();
$this->tfa = new TwoFactorAuth('MONARC', 6, 30, 'sha1', $qr);
}
Expand Down Expand Up @@ -69,7 +60,6 @@ public function authenticate($data): array
$token = $data['recoveryCode'];
}


if (isset($data['verificationCode']) && isset($data['otpSecret'])) {
// activation of 2FA via login page (when user must activate 2FA on a 2FA enforced instance)
$token = $data['otpSecret'] . ':' . $data['verificationCode'];
Expand Down
Loading

0 comments on commit 681856c

Please sign in to comment.