diff --git a/config/services.yaml b/config/services.yaml index fb43656..f3fd291 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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)%' diff --git a/docs/env.example b/docs/env.example index c4b6ca2..73cd315 100644 --- a/docs/env.example +++ b/docs/env.example @@ -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 " it returns "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 diff --git a/src/ConfigBag.php b/src/ConfigBag.php index 66d022f..d759cbb 100644 --- a/src/ConfigBag.php +++ b/src/ConfigBag.php @@ -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; } diff --git a/src/Listener/LoginListener.php b/src/Listener/LoginListener.php index 9ab37b9..3d288f0 100644 --- a/src/Listener/LoginListener.php +++ b/src/Listener/LoginListener.php @@ -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'); - } } diff --git a/src/Listener/LookupListener.php b/src/Listener/LookupListener.php new file mode 100644 index 0000000..0b5526e --- /dev/null +++ b/src/Listener/LookupListener.php @@ -0,0 +1,41 @@ +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 " it returns + * "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'] + )); + } + } + } +} diff --git a/src/Trait/GetTotpTrait.php b/src/Trait/GetTotpTrait.php new file mode 100644 index 0000000..1a51526 --- /dev/null +++ b/src/Trait/GetTotpTrait.php @@ -0,0 +1,22 @@ +config->totpUri(), $this->config->clock() + ); + if ($otp instanceof TOTPInterface) { + return $otp; + } + throw new HttpException(500, 'Internal Server Exception'); + } +}