v0.6.0 optional lookup for totp by static password
This commit is contained in:
@@ -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