Skip to content

Commit

Permalink
feat(checkpoints): Security gates
Browse files Browse the repository at this point in the history
Checkpoints can be referred to as security gates, the authentication process has to successfully pass through every single gate defined in order to be granted access.

By default, when logging in, checks for existing sessions and failed logins occur, you may configure an indefinite number of "checkpoints".
  • Loading branch information
trants committed Jul 1, 2024
1 parent 70ac87c commit 2194183
Show file tree
Hide file tree
Showing 8 changed files with 677 additions and 0 deletions.
75 changes: 75 additions & 0 deletions src/Checkpoints/ActivationCheckpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/*
* Copyright (c) 2024 Focela Technologies. All rights reserved.
* Use of this source code is governed by a MIT style
* license that can be found in the LICENSE file.
*/

namespace Focela\Laratrust\Checkpoints;

use Focela\Laratrust\Users\UserInterface;
use Focela\Laratrust\Activations\ActivationRepositoryInterface;

class ActivationCheckpoint implements CheckpointInterface
{
use AuthenticatedCheckpoint;

/**
* The Activations repository instance.
*
* @var ActivationRepositoryInterface
*/
protected $activations;

/**
* Constructor.
*
* @param ActivationRepositoryInterface $activations
*
* @return void
*/
public function __construct(ActivationRepositoryInterface $activations)
{
$this->activations = $activations;
}

/**
* @inheritdoc
*/
public function login(UserInterface $user): bool
{
return $this->checkActivation($user);
}

/**
* Checks the activation status of the given user.
*
* @param UserInterface $user
*
* @throws NotActivatedException
*
* @return bool
*/
protected function checkActivation(UserInterface $user): bool
{
$completed = $this->activations->completed($user);

if (! $completed) {
$exception = new NotActivatedException('Your account has not been activated yet.');

$exception->setUser($user);

throw $exception;
}

return true;
}

/**
* @inheritdoc
*/
public function check(UserInterface $user): bool
{
return $this->checkActivation($user);
}
}
21 changes: 21 additions & 0 deletions src/Checkpoints/AuthenticatedCheckpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php
/*
* Copyright (c) 2024 Focela Technologies. All rights reserved.
* Use of this source code is governed by a MIT style
* license that can be found in the LICENSE file.
*/

namespace Focela\Laratrust\Checkpoints;

use Focela\Laratrust\Users\UserInterface;

trait AuthenticatedCheckpoint
{
/**
* @inheritdoc
*/
public function fail(?UserInterface $user = null): bool
{
return true;
}
}
42 changes: 42 additions & 0 deletions src/Checkpoints/CheckpointInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/*
* Copyright (c) 2024 Focela Technologies. All rights reserved.
* Use of this source code is governed by a MIT style
* license that can be found in the LICENSE file.
*/

namespace Focela\Laratrust\Checkpoints;

use Focela\Laratrust\Users\UserInterface;

interface CheckpointInterface
{
/**
* Checkpoint after a user is logged in. Return false to deny persistence.
*
* @param UserInterface $user
*
* @return bool
*/
public function login(UserInterface $user): bool;

/**
* Checkpoint for when a user is currently stored in the session.
*
* @param UserInterface $user
*
* @return bool
*/
public function check(UserInterface $user): bool;

/**
* Checkpoint for when a failed login attempt is logged. User is not always
* passed and the result of the method will not affect anything, as the
* login failed.
*
* @param UserInterface|null $user
*
* @return bool
*/
public function fail(?UserInterface $user = null): bool;
}
42 changes: 42 additions & 0 deletions src/Checkpoints/NotActivatedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
/*
* Copyright (c) 2024 Focela Technologies. All rights reserved.
* Use of this source code is governed by a MIT style
* license that can be found in the LICENSE file.
*/

namespace Focela\Laratrust\Checkpoints;

use Focela\Laratrust\Users\UserInterface;

class NotActivatedException extends \RuntimeException
{
/**
* The user which caused the exception.
*
* @var UserInterface
*/
protected $user;

/**
* Returns the user.
*
* @return UserInterface
*/
public function getUser(): UserInterface
{
return $this->user;
}

/**
* Sets the user associated with Laratrust (does not log in).
*
* @param UserInterface
*
* @return void
*/
public function setUser(UserInterface $user): void
{
$this->user = $user;
}
}
148 changes: 148 additions & 0 deletions src/Checkpoints/ThrottleCheckpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php
/*
* Copyright (c) 2024 Focela Technologies. All rights reserved.
* Use of this source code is governed by a MIT style
* license that can be found in the LICENSE file.
*/

namespace Focela\Laratrust\Checkpoints;

use Focela\Laratrust\Users\UserInterface;
use Focela\Laratrust\Throttling\ThrottleRepositoryInterface;

class ThrottleCheckpoint implements CheckpointInterface
{
/**
* The Throttle repository instance.
*
* @var ThrottleRepositoryInterface
*/
protected $throttle;

/**
* The cached IP address, used for checkpoints checks.
*
* @var string
*/
protected $ipAddress;

/**
* Constructor.
*
* @param ThrottleRepositoryInterface $throttle
* @param string $ipAddress
*
* @return void
*/
public function __construct(ThrottleRepositoryInterface $throttle, $ipAddress = null)
{
$this->throttle = $throttle;

if (isset($ipAddress)) {
$this->ipAddress = $ipAddress;
}
}

/**
* @inheritdoc
*/
public function login(UserInterface $user): bool
{
return $this->checkThrottling('login', $user);
}

/**
* Checks the throttling status of the given user.
*
* @param string $action
* @param UserInterface|null $user
*
* @return bool
*/
protected function checkThrottling(string $action, ?UserInterface $user = null): bool
{
// If we are just checking an existing logged in person, the global delay
// shouldn't stop them being logged in at all. Only their IP address and
// user a
if ($action === 'login') {
$globalDelay = $this->throttle->globalDelay();

if ($globalDelay > 0) {
$this->throwException("Too many unsuccessful attempts have been made globally, logins are locked for another [{$globalDelay}] second(s).", 'global', $globalDelay);
}
}

// Suspicious activity from a single IP address will not only lock
// logins but also any logged in users from that IP address. This
// should deter a single hacker who may have guessed a password
// within the configured throttling limit.
if (isset($this->ipAddress)) {
$ipDelay = $this->throttle->ipDelay($this->ipAddress);

if ($ipDelay > 0) {
$this->throwException("Suspicious activity has occured on your IP address and you have been denied access for another [{$ipDelay}] second(s).", 'ip', $ipDelay);
}
}

// We will only suspend people logging into a user account. This will
// leave the logged in user unaffected. Picture a famous person who's
// account is being locked as they're logged in, purely because
// others are trying to hack it.
if ($action === 'login' && isset($user)) {
$userDelay = $this->throttle->userDelay($user);

if ($userDelay > 0) {
$this->throwException("Too many unsuccessful login attempts have been made against your account. Please try again after another [{$userDelay}] second(s).", 'user', $userDelay);
}
}

return true;
}

/**
* Throws a throttling exception.
*
* @param string $message
* @param string $type
* @param int $delay
*
* @throws ThrottlingException
*
* @return void
*/
protected function throwException(string $message, string $type, int $delay): void
{
$exception = new ThrottlingException($message);

$exception->setDelay($delay);

$exception->setType($type);

throw $exception;
}

/**
* @inheritdoc
*/
public function check(UserInterface $user): bool
{
return $this->checkThrottling('check', $user);
}

/**
* @inheritdoc
*/
public function fail(?UserInterface $user = null): bool
{
// We'll check throttling firstly from any previous attempts. This
// will throw the required exceptions if the user has already
// tried to login too many times.
$this->checkThrottling('login', $user);

// Now we've checked previous attempts, we'll log this latest attempt.
// It'll be picked up the next time if the user tries again.
$this->throttle->log($this->ipAddress, $user);

return true;
}
}
Loading

0 comments on commit 2194183

Please sign in to comment.