Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27394ae555 |
@@ -30,9 +30,9 @@ parameters:
|
|||||||
# could be useful if you have a system which does not handle cookies
|
# could be useful if you have a system which does not handle cookies
|
||||||
env(IP_TTL): '0' # default disabled, time in seconds
|
env(IP_TTL): '0' # default disabled, time in seconds
|
||||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
# 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_ENABLED): '0' # boolean
|
||||||
env(STATIC_SECRET): '' # default disabled
|
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"
|
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||||
env(TEAPOT): '1' # boolean
|
env(TEAPOT): '1' # boolean
|
||||||
|
|
||||||
@@ -63,6 +63,7 @@ parameters:
|
|||||||
app.ip_ttl: '%env(IP_TTL)%'
|
app.ip_ttl: '%env(IP_TTL)%'
|
||||||
app.static_secret_enabled: '%env(STATIC_SECRET_ENABLED)%'
|
app.static_secret_enabled: '%env(STATIC_SECRET_ENABLED)%'
|
||||||
app.static_secret: '%env(STATIC_SECRET)%'
|
app.static_secret: '%env(STATIC_SECRET)%'
|
||||||
|
app.lookup_totp: '%env(LOOKUP_TOTP)%'
|
||||||
app.teapot: '%env(TEAPOT)%'
|
app.teapot: '%env(TEAPOT)%'
|
||||||
|
|
||||||
app.error_message: '%env(ERROR_MESSAGE)%'
|
app.error_message: '%env(ERROR_MESSAGE)%'
|
||||||
|
|||||||
@@ -27,8 +27,12 @@
|
|||||||
#IP_TTL=0 # default disabled, time in seconds
|
#IP_TTL=0 # default disabled, time in seconds
|
||||||
|
|
||||||
# if desired, in addition to supporting a TOTP, you can set a static password
|
# 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_ENABLED='0' # boolean, disabled by default
|
||||||
#STATIC_SECRET='' # default disabled
|
#STATIC_SECRET='' # default disabled
|
||||||
|
#LOOKUP_TOTP='' # default disabled
|
||||||
|
|
||||||
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
|
||||||
#TEAPOT=true # default enabled, boolean
|
#TEAPOT=true # default enabled, boolean
|
||||||
@@ -40,6 +44,7 @@
|
|||||||
#ERROR_COLOR='#ffb16d' # apricot (light orange)
|
#ERROR_COLOR='#ffb16d' # apricot (light orange)
|
||||||
#ID_NAME='Session ID'
|
#ID_NAME='Session ID'
|
||||||
#TOKEN_NAME='Authentication Token'
|
#TOKEN_NAME='Authentication Token'
|
||||||
|
PASSWORD_NAME='Authentication Password'
|
||||||
#SUBMIT_NAME='Submit'
|
#SUBMIT_NAME='Submit'
|
||||||
#ERROR_MESSAGE='Unsuccessful login attempt'
|
#ERROR_MESSAGE='Unsuccessful login attempt'
|
||||||
# title and message to use on block page, if teapot is true
|
# 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 $queryPrefix;
|
||||||
private string $totpUri;
|
private string $totpUri;
|
||||||
private ?int $ipTtl;
|
private ?int $ipTtl;
|
||||||
|
private ?string $lookupTotp;
|
||||||
private ?string $staticSecret;
|
private ?string $staticSecret;
|
||||||
private bool $teapot;
|
private bool $teapot;
|
||||||
private string $errorMessage;
|
private string $errorMessage;
|
||||||
@@ -33,6 +34,7 @@ final readonly class ConfigBag {
|
|||||||
#[Autowire('%app.query_prefix%')] string $queryPrefix,
|
#[Autowire('%app.query_prefix%')] string $queryPrefix,
|
||||||
#[Autowire('%app.totp_uri%')] string $totpUri,
|
#[Autowire('%app.totp_uri%')] string $totpUri,
|
||||||
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
|
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
|
||||||
|
#[Autowire('%app.lookup_totp%')] ?string $lookupTotp,
|
||||||
#[Autowire('%app.static_secret_enabled%')] bool $staticSecretEnabled,
|
#[Autowire('%app.static_secret_enabled%')] bool $staticSecretEnabled,
|
||||||
#[Autowire('%app.static_secret%')] ?string $staticSecret,
|
#[Autowire('%app.static_secret%')] ?string $staticSecret,
|
||||||
#[Autowire('%app.teapot%')] bool $teapot,
|
#[Autowire('%app.teapot%')] bool $teapot,
|
||||||
@@ -49,6 +51,7 @@ final readonly class ConfigBag {
|
|||||||
$this->totpUri = $totpUri ?: $utilities->loadTotp();
|
$this->totpUri = $totpUri ?: $utilities->loadTotp();
|
||||||
$this->ipTtl = $ipTtl ?: null;
|
$this->ipTtl = $ipTtl ?: null;
|
||||||
$this->staticSecret = $staticSecretEnabled ? ($staticSecret ?: null) : null;
|
$this->staticSecret = $staticSecretEnabled ? ($staticSecret ?: null) : null;
|
||||||
|
$this->lookupTotp = $this->staticSecret ? ($lookupTotp ?: null) : null;
|
||||||
$this->teapot = $teapot;
|
$this->teapot = $teapot;
|
||||||
$this->errorMessage = $errorMessage;
|
$this->errorMessage = $errorMessage;
|
||||||
$this->teapotTitle = $teapotTitle;
|
$this->teapotTitle = $teapotTitle;
|
||||||
@@ -87,6 +90,10 @@ final readonly class ConfigBag {
|
|||||||
return $this->ipTtl;
|
return $this->ipTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function lookupTotp(): ?string {
|
||||||
|
return $this->lookupTotp;
|
||||||
|
}
|
||||||
|
|
||||||
public function staticSecret(): ?string {
|
public function staticSecret(): ?string {
|
||||||
return $this->staticSecret;
|
return $this->staticSecret;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ use App\Data\Payload;
|
|||||||
use App\Enum\Scope;
|
use App\Enum\Scope;
|
||||||
use App\MonitorCacheKeys;
|
use App\MonitorCacheKeys;
|
||||||
use App\Trait\CookieNameTrait;
|
use App\Trait\CookieNameTrait;
|
||||||
|
use App\Trait\GetTotpTrait;
|
||||||
use App\Trait\MakeNonceTrait;
|
use App\Trait\MakeNonceTrait;
|
||||||
use App\Trait\StringTrait;
|
use App\Trait\StringTrait;
|
||||||
use OTPHP\Factory;
|
|
||||||
use OTPHP\TOTPInterface;
|
|
||||||
use Psr\Cache\CacheItemPoolInterface;
|
use Psr\Cache\CacheItemPoolInterface;
|
||||||
use Psr\Cache\InvalidArgumentException;
|
use Psr\Cache\InvalidArgumentException;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
@@ -31,19 +30,21 @@ final readonly class LoginListener {
|
|||||||
use CookieNameTrait;
|
use CookieNameTrait;
|
||||||
use MakeNonceTrait;
|
use MakeNonceTrait;
|
||||||
use StringTrait;
|
use StringTrait;
|
||||||
|
use GetTotpTrait;
|
||||||
|
|
||||||
private CacheItemPoolInterface $requestPool;
|
private CacheItemPoolInterface $requestPool;
|
||||||
private CacheItemPoolInterface $sessionPool;
|
private CacheItemPoolInterface $sessionPool;
|
||||||
|
|
||||||
/** @throws InvalidArgumentException */
|
/** @throws InvalidArgumentException */
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ConfigBag $config,
|
ConfigBag $config,
|
||||||
private Environment $twig,
|
private Environment $twig,
|
||||||
CacheItemPoolInterface $noncePool,
|
CacheItemPoolInterface $noncePool,
|
||||||
CacheItemPoolInterface $requestPool,
|
CacheItemPoolInterface $requestPool,
|
||||||
CacheItemPoolInterface $sessionPool,
|
CacheItemPoolInterface $sessionPool,
|
||||||
LoggerInterface $logger,
|
LoggerInterface $logger,
|
||||||
) {
|
) {
|
||||||
|
$this->config = $config;
|
||||||
$this->requestPool = new MonitorCacheKeys($requestPool);
|
$this->requestPool = new MonitorCacheKeys($requestPool);
|
||||||
$this->sessionPool = new MonitorCacheKeys($sessionPool);
|
$this->sessionPool = new MonitorCacheKeys($sessionPool);
|
||||||
$this->noncePool = $noncePool;
|
$this->noncePool = $noncePool;
|
||||||
@@ -98,7 +99,7 @@ final readonly class LoginListener {
|
|||||||
if ($nonceItem->isHit() && $nonceItem->get()) {
|
if ($nonceItem->isHit() && $nonceItem->get()) {
|
||||||
/* mark nonce as spent */
|
/* mark nonce as spent */
|
||||||
$nonceItem->set(false); /* invalid */
|
$nonceItem->set(false); /* invalid */
|
||||||
$nonceItem->expiresAfter(static::NONCE_TTL); /* keep breifly */
|
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
||||||
$this->noncePool->save($nonceItem);
|
$this->noncePool->save($nonceItem);
|
||||||
|
|
||||||
/* token authentication successful, grant access and set response */
|
/* token authentication successful, grant access and set response */
|
||||||
@@ -159,14 +160,15 @@ final readonly class LoginListener {
|
|||||||
if (($nonceItem->isHit() && $nonceItem->get()) || ! $nonceItem->isHit()) {
|
if (($nonceItem->isHit() && $nonceItem->get()) || ! $nonceItem->isHit()) {
|
||||||
/* mark nonce as spent */
|
/* mark nonce as spent */
|
||||||
$nonceItem->set(false); /* invalid */
|
$nonceItem->set(false); /* invalid */
|
||||||
$nonceItem->expiresAfter(static::NONCE_TTL); /* keep breifly */
|
$nonceItem->expiresAfter(LoginListener::NONCE_TTL); /* keep briefly */
|
||||||
$this->noncePool->save($nonceItem);
|
$this->noncePool->save($nonceItem);
|
||||||
|
|
||||||
/* password authentication successful, grant access and set response */
|
/* password authentication successful, grant access and set response */
|
||||||
$cleanId = $this->makeCacheKey($payload->id);
|
$cleanId = $this->makeCacheKey($payload->id);
|
||||||
$this->logger->debug("successful login for: $cleanId");
|
$this->logger->debug("successful login for: $cleanId");
|
||||||
return new Response("hi $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]);
|
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