Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27394ae555 |
@@ -30,9 +30,9 @@ parameters:
|
||||
# could be useful if you have a system which does not handle cookies
|
||||
env(IP_TTL): '0' # default disabled, time in seconds
|
||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
||||
# TODO rely on checking enabled, instead of the secret directly throughout the code
|
||||
env(STATIC_SECRET_ENABLED): '0' # boolean
|
||||
env(STATIC_SECRET): '' # default disabled
|
||||
env(LOOKUP_TOTP): '' # default disabled
|
||||
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||
env(TEAPOT): '1' # boolean
|
||||
|
||||
@@ -63,6 +63,7 @@ parameters:
|
||||
app.ip_ttl: '%env(IP_TTL)%'
|
||||
app.static_secret_enabled: '%env(STATIC_SECRET_ENABLED)%'
|
||||
app.static_secret: '%env(STATIC_SECRET)%'
|
||||
app.lookup_totp: '%env(LOOKUP_TOTP)%'
|
||||
app.teapot: '%env(TEAPOT)%'
|
||||
|
||||
app.error_message: '%env(ERROR_MESSAGE)%'
|
||||
|
||||
@@ -27,8 +27,12 @@
|
||||
#IP_TTL=0 # default disabled, time in seconds
|
||||
|
||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
||||
# if you enable a static secret, you may choose to also enable a totp-lookup,
|
||||
# which contains a special path, when successfully authenticated,
|
||||
# instead of returning "hi <your-name>" it returns "code <totp-code>".
|
||||
#STATIC_SECRET_ENABLED='0' # boolean, disabled by default
|
||||
#STATIC_SECRET='' # default disabled
|
||||
#LOOKUP_TOTP='' # default disabled
|
||||
|
||||
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||
#TEAPOT=true # default enabled, boolean
|
||||
@@ -40,6 +44,7 @@
|
||||
#ERROR_COLOR='#ffb16d' # apricot (light orange)
|
||||
#ID_NAME='Session ID'
|
||||
#TOKEN_NAME='Authentication Token'
|
||||
PASSWORD_NAME='Authentication Password'
|
||||
#SUBMIT_NAME='Submit'
|
||||
#ERROR_MESSAGE='Unsuccessful login attempt'
|
||||
# title and message to use on block page, if teapot is true
|
||||
|
||||
@@ -16,6 +16,7 @@ final readonly class ConfigBag {
|
||||
private string $queryPrefix;
|
||||
private string $totpUri;
|
||||
private ?int $ipTtl;
|
||||
private ?string $lookupTotp;
|
||||
private ?string $staticSecret;
|
||||
private bool $teapot;
|
||||
private string $errorMessage;
|
||||
@@ -33,6 +34,7 @@ final readonly class ConfigBag {
|
||||
#[Autowire('%app.query_prefix%')] string $queryPrefix,
|
||||
#[Autowire('%app.totp_uri%')] string $totpUri,
|
||||
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
|
||||
#[Autowire('%app.lookup_totp%')] ?string $lookupTotp,
|
||||
#[Autowire('%app.static_secret_enabled%')] bool $staticSecretEnabled,
|
||||
#[Autowire('%app.static_secret%')] ?string $staticSecret,
|
||||
#[Autowire('%app.teapot%')] bool $teapot,
|
||||
@@ -49,6 +51,7 @@ final readonly class ConfigBag {
|
||||
$this->totpUri = $totpUri ?: $utilities->loadTotp();
|
||||
$this->ipTtl = $ipTtl ?: null;
|
||||
$this->staticSecret = $staticSecretEnabled ? ($staticSecret ?: null) : null;
|
||||
$this->lookupTotp = $this->staticSecret ? ($lookupTotp ?: null) : null;
|
||||
$this->teapot = $teapot;
|
||||
$this->errorMessage = $errorMessage;
|
||||
$this->teapotTitle = $teapotTitle;
|
||||
@@ -87,6 +90,10 @@ final readonly class ConfigBag {
|
||||
return $this->ipTtl;
|
||||
}
|
||||
|
||||
public function lookupTotp(): ?string {
|
||||
return $this->lookupTotp;
|
||||
}
|
||||
|
||||
public function staticSecret(): ?string {
|
||||
return $this->staticSecret;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,9 @@ use App\Data\Payload;
|
||||
use App\Enum\Scope;
|
||||
use App\MonitorCacheKeys;
|
||||
use App\Trait\CookieNameTrait;
|
||||
use App\Trait\GetTotpTrait;
|
||||
use App\Trait\MakeNonceTrait;
|
||||
use App\Trait\StringTrait;
|
||||
use OTPHP\Factory;
|
||||
use OTPHP\TOTPInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -31,19 +30,21 @@ final readonly class LoginListener {
|
||||
use CookieNameTrait;
|
||||
use MakeNonceTrait;
|
||||
use StringTrait;
|
||||
use GetTotpTrait;
|
||||
|
||||
private CacheItemPoolInterface $requestPool;
|
||||
private CacheItemPoolInterface $sessionPool;
|
||||
|
||||
/** @throws InvalidArgumentException */
|
||||
public function __construct(
|
||||
private ConfigBag $config,
|
||||
ConfigBag $config,
|
||||
private Environment $twig,
|
||||
CacheItemPoolInterface $noncePool,
|
||||
CacheItemPoolInterface $requestPool,
|
||||
CacheItemPoolInterface $sessionPool,
|
||||
LoggerInterface $logger,
|
||||
) {
|
||||
$this->config = $config;
|
||||
$this->requestPool = new MonitorCacheKeys($requestPool);
|
||||
$this->sessionPool = new MonitorCacheKeys($sessionPool);
|
||||
$this->noncePool = $noncePool;
|
||||
@@ -98,7 +99,7 @@ final readonly class LoginListener {
|
||||
if ($nonceItem->isHit() && $nonceItem->get()) {
|
||||
/* mark nonce as spent */
|
||||
$nonceItem->set(false); /* invalid */
|
||||
$nonceItem->expiresAfter(static::NONCE_TTL); /* keep breifly */
|
||||
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
||||
$this->noncePool->save($nonceItem);
|
||||
|
||||
/* token authentication successful, grant access and set response */
|
||||
@@ -159,14 +160,15 @@ final readonly class LoginListener {
|
||||
if (($nonceItem->isHit() && $nonceItem->get()) || ! $nonceItem->isHit()) {
|
||||
/* mark nonce as spent */
|
||||
$nonceItem->set(false); /* invalid */
|
||||
$nonceItem->expiresAfter(static::NONCE_TTL); /* keep breifly */
|
||||
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
||||
$this->noncePool->save($nonceItem);
|
||||
|
||||
/* password authentication successful, grant access and set response */
|
||||
$cleanId = $this->makeCacheKey($payload->id);
|
||||
$this->logger->debug("successful login for: $cleanId");
|
||||
return new Response("hi $cleanId",
|
||||
headers: ['Content-Type' => 'text/plain']
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/plain']
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -254,14 +256,4 @@ final readonly class LoginListener {
|
||||
|
||||
return new Response($content, $status, ["Content-Type" => $contentType]);
|
||||
}
|
||||
|
||||
private function getTotp(): TOTPInterface {
|
||||
$otp = Factory::loadFromProvisioningUri(
|
||||
$this->config->totpUri(), $this->config->clock()
|
||||
);
|
||||
if ($otp instanceof TOTPInterface) {
|
||||
return $otp;
|
||||
}
|
||||
throw new HttpException(500, 'Internal Server Exception');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Listener;
|
||||
|
||||
use App\ConfigBag;
|
||||
use App\Trait\GetTotpTrait;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Event\ResponseEvent;
|
||||
|
||||
final readonly class LookupListener {
|
||||
use GetTotpTrait;
|
||||
|
||||
public function __construct(
|
||||
ConfigBag $config,
|
||||
) {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
#[AsEventListener(priority: 44)]
|
||||
public function onKernelResponse(ResponseEvent $event): void {
|
||||
/* if you enable a static secret, you may choose to also enable
|
||||
* a totp-lookup, which contains a special path, when successfully
|
||||
* authenticated, instead of returning "hi <your-name>" it returns
|
||||
* "code <totp-code>" */
|
||||
if ($this->config->staticSecret() && $this->config->lookupTotp()) {
|
||||
if (str_contains($event->getRequest()->getPathInfo(),
|
||||
$this->config->lookupTotp()) &&
|
||||
$event->getResponse()->getStatusCode() === Response::HTTP_OK
|
||||
) {
|
||||
$totp = $this->getTotp();
|
||||
$next = $totp->at(time() + $totp->getPeriod());
|
||||
$event->setResponse(new Response(
|
||||
"next {$next}",
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/plain']
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Trait;
|
||||
|
||||
use App\ConfigBag;
|
||||
use OTPHP\Factory;
|
||||
use OTPHP\TOTPInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
trait GetTotpTrait {
|
||||
protected readonly ConfigBag $config;
|
||||
|
||||
protected function getTotp(): TOTPInterface {
|
||||
$otp = Factory::loadFromProvisioningUri(
|
||||
$this->config->totpUri(), $this->config->clock()
|
||||
);
|
||||
if ($otp instanceof TOTPInterface) {
|
||||
return $otp;
|
||||
}
|
||||
throw new HttpException(500, 'Internal Server Exception');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user