Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions code_examples/totp/_misc/totp-append-ConfigProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
GetEnableTotpFormHandler::class => AttributedServiceFactory::class,
PostEnableTotpHandler::class => AttributedServiceFactory::class,
GetDisableTotpFormHandler::class => AttributedServiceFactory::class,
PostDisableTotpHandler::class => AttributedServiceFactory::class,
PostValidateTotpHandler::class => AttributedServiceFactory::class,
GetTotpHandler::class => AttributedServiceFactory::class,
GetRecoveryFormHandler::class => AttributedServiceFactory::class,
PostValidateRecoveryHandler::class => AttributedServiceFactory::class,

TotpForm::class => ElementFactory::class,
RecoveryForm::class => ElementFactory::class,
2 changes: 2 additions & 0 deletions code_examples/totp/_misc/totp-append-Pipeline.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
$app->pipe(TotpMiddleware::class);
$app->pipe(CancelUrlMiddleware::class);
8 changes: 8 additions & 0 deletions code_examples/totp/_misc/totp-append-routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
->get('/enable-totp-form', GetEnableTotpFormHandler::class, 'admin::enable-totp-form')
->post('/enable-totp', PostEnableTotpHandler::class, 'admin::enable-totp')
->get('/disable-totp-form', GetDisableTotpFormHandler::class, 'admin::disable-totp-form')
->post('/disable-totp', PostDisableTotpHandler::class, 'admin::disable-totp')
->get('/validate-totp-form', GetTotpHandler::class, 'admin::validate-totp-form')
->post('/validate-totp', PostValidateTotpHandler::class, 'admin::validate-totp')
->get('/recovery-form', GetRecoveryFormHandler::class, 'admin::recovery-form')
->post('/validate-recovery', PostValidateRecoveryHandler::class, 'admin::validate-recovery')
20 changes: 20 additions & 0 deletions code_examples/totp/_misc/totp-append-view-account.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="bgc-white p-20 bd">
<h6 class="c-grey-900">TOTP Setup</h6>
{% if isTotpEnabled %}
<h6 class="c-grey-900">TOTP is enabled</h6>
<div class="d-flex justify-content-end">
<a href="{{ path('admin::disable-totp-form') }}"
class="btn btn-primary btn-color btn-sm">
Disable TOTP
</a>
</div>
{% else %}
<h6 class="c-grey-900">TOTP is disabled</h6>
<div class="d-flex justify-content-end">
<a href="{{ path('admin::enable-totp-form') }}"
class="btn btn-primary btn-color btn-sm">
Enable TOTP
</a>
</div>
{% endif %}
</div>
28 changes: 28 additions & 0 deletions code_examples/totp/config/autoload/totp.global.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

use Core\Admin\Entity\Admin;
use Core\Admin\Entity\AdminIdentity;

return [
'dot_totp' => [
'identity_class_map' => [
AdminIdentity::class => Admin::class,
],
'totp_required_routes' => [
'page::components' => true,
],
'options' => [
// Time step in seconds
'period' => 30,
// Number of digits in the TOTP code
'digits' => 6,
// Hashing algorithm used to generate the code
'algorithm' => 'sha1',
],
'provision_uri_config' => [
'issuer' => 'DK-Admin',
],
],
];
77 changes: 77 additions & 0 deletions code_examples/totp/src/Admin/src/Form/RecoveryForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Admin\Admin\Form;

use Admin\Admin\InputFilter\RecoveryInputFilter;
use Admin\App\Form\AbstractForm;
use Dot\DependencyInjection\Attribute\Inject;
use Laminas\Form\Element\Csrf;
use Laminas\Form\Element\Submit;
use Laminas\Form\Element\Text;
use Laminas\Form\Exception\ExceptionInterface;
use Laminas\Session\Container;
use Mezzio\Router\RouterInterface;

/**
* @phpstan-import-type RecoveryDataType from RecoveryInputFilter
* @extends AbstractForm<RecoveryDataType>
*/
class RecoveryForm extends AbstractForm
{
/**
* @param array<non-empty-string, mixed> $options
* @throws ExceptionInterface
*/
#[Inject(
RouterInterface::class,
)]
public function __construct(?string $name = null, array $options = [])
{
parent::__construct($name, $options);

$this->init();

$this->setAttribute('id', 'recovery-form');
$this->setAttribute('method', 'post');

$this->inputFilter = new RecoveryInputFilter();
$this->inputFilter->init();
}

/**
* @throws ExceptionInterface
*/
public function init(): void
{
$this->add(
(new Text('recoveryCode'))
->setLabel('Recovery Code')
->setAttribute('class', 'form-control')
->setAttribute('minlength', 11)
->setAttribute('maxlength', 11)
->setAttribute('pattern', '[A-Za-z0-9]{5}-[A-Za-z0-9]{5}')
->setAttribute('required', true)
->setAttribute('autofocus', true)
);

$this->add(
(new Csrf('recoveryCsrf'))
->setOptions([
'csrf_options' => [
'timeout' => 3600,
'session' => new Container(),
],
])
->setAttribute('required', true)
);

$this->add(
(new Submit('submit'))
->setAttribute('type', 'submit')
->setAttribute('value', 'Verify Code')
->setAttribute('class', 'btn btn-primary mt-2')
);
}
}
77 changes: 77 additions & 0 deletions code_examples/totp/src/Admin/src/Form/TotpForm.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace Admin\Admin\Form;

use Admin\Admin\InputFilter\TotpInputFilter;
use Admin\App\Form\AbstractForm;
use Dot\DependencyInjection\Attribute\Inject;
use Laminas\Form\Element\Csrf;
use Laminas\Form\Element\Submit;
use Laminas\Form\Element\Text;
use Laminas\Form\Exception\ExceptionInterface;
use Laminas\Session\Container;
use Mezzio\Router\RouterInterface;

/**
* @phpstan-import-type TotpDataType from TotpInputFilter
* @extends AbstractForm<TotpDataType>
*/
class TotpForm extends AbstractForm
{
/**
* @param array<non-empty-string, mixed> $options
* @throws ExceptionInterface
*/
#[Inject(
RouterInterface::class,
)]
public function __construct(?string $name = null, array $options = [])
{
parent::__construct($name, $options);

$this->init();

$this->setAttribute('id', 'enable-totp-form');
$this->setAttribute('method', 'post');
$this->setAttribute('title', 'TOTP Authentication Setup');

$this->inputFilter = new TotpInputFilter();
$this->inputFilter->init();
}

/**
* @throws ExceptionInterface
*/
public function init(): void
{
$this->add(
(new Text('code'))
->setLabel('Authentication Code')
->setAttribute('class', 'form-control')
->setAttribute('maxlength', 6)
->setAttribute('pattern', '\d{6}')
->setAttribute('required', true)
->setAttribute('autofocus', true)
);

$this->add(
(new Csrf('totpCsrf'))
->setOptions([
'csrf_options' => [
'timeout' => 3600,
'session' => new Container(),
],
])
->setAttribute('required', true)
);

$this->add(
(new Submit('submit'))
->setAttribute('type', 'submit')
->setAttribute('value', 'Verify Code')
->setAttribute('class', 'btn btn-primary mt-2')
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Admin\Admin\Handler\Account;

use Admin\Admin\Form\TotpForm;
use Dot\DependencyInjection\Attribute\Inject;
use Laminas\Authentication\AuthenticationService;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Router\RouterInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;

class GetDisableTotpFormHandler implements RequestHandlerInterface
{
#[Inject(
TemplateRendererInterface::class,
TotpForm::class,
RouterInterface::class,
AuthenticationService::class,
)]
public function __construct(
protected TemplateRendererInterface $template,
protected TotpForm $totpForm,
protected RouterInterface $router,
protected AuthenticationService $authenticationService,
) {
}

public function handle(ServerRequestInterface $request): ResponseInterface|EmptyResponse|HtmlResponse
{
$this->totpForm
->setAttribute('action', $this->router->generateUri('admin::disable-totp'));

return new HtmlResponse(
$this->template->render('admin::validate-totp-form', [
'totpForm' => $this->totpForm->prepare(),
'cancelUrl' => $this->router->generateUri('admin::edit-account'),
'error' => null,
])
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace Admin\Admin\Handler\Account;

use Admin\Admin\Form\TotpForm;
use Dot\DependencyInjection\Attribute\Inject;
use Dot\FlashMessenger\FlashMessengerInterface;
use Dot\Totp\Totp;
use Laminas\Authentication\AuthenticationService;
use Laminas\Authentication\Exception\ExceptionInterface;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\HtmlResponse;
use Mezzio\Router\RouterInterface;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Random\RandomException;

use function time;

class GetEnableTotpFormHandler implements RequestHandlerInterface
{
private const int SECRET_MAX_AGE = 600;

/**
* @param array{label: string, issuer: string} $provisioningUri
*/
#[Inject(
Totp::class,
AuthenticationService::class,
FlashMessengerInterface::class,
TemplateRendererInterface::class,
TotpForm::class,
RouterInterface::class,
'config.dot_totp.provision_uri_config'
)]
public function __construct(
protected Totp $totpService,
protected AuthenticationService $authenticationService,
protected FlashMessengerInterface $messenger,
protected TemplateRendererInterface $template,
protected TotpForm $totpForm,
protected RouterInterface $router,
protected array $provisioningUri
) {
}

/**
* @throws ExceptionInterface
* @throws RandomException
*/
public function handle(ServerRequestInterface $request): ResponseInterface|EmptyResponse|HtmlResponse
{
$storage = $this->authenticationService->getStorage()->read();

if (
empty($storage->pendingSecret) ||
empty($storage->secretTimestamp) ||
(time() - $storage->secretTimestamp) > self::SECRET_MAX_AGE
) {
$storage->pendingSecret = $this->totpService->generateSecretBase32();
$storage->secretTimestamp = time();
$this->authenticationService->getStorage()->write($storage);
}

$uri = $this->totpService->getProvisioningUri(
$storage->getIdentity(),
$this->provisioningUri['issuer'],
$storage->pendingSecret
);

$qrSvg = $this->totpService->generateInlineSvgQr($uri);
$storage->totp_verified = false;

if (isset($storage->recovery_auth) && $storage->recovery_auth) {
$this->totpForm->setAttribute('title', 'Reconfigure Two-Factor Authentication');
}

$this->totpForm->setAttribute('action', $this->router->generateUri('admin::enable-totp'));

return new HtmlResponse(
$this->template->render('admin::validate-totp-form', [
'qrSvg' => $qrSvg,
'cancelUrl' => $request->getAttribute('cancelUrl'),
'totpForm' => $this->totpForm->prepare(),
'error' => null,
])
);
}
}
Loading