From db72dac2436db8ac684e74152cc83b4028776f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 28 Apr 2024 17:50:19 +0200 Subject: [PATCH] Save the date when a webauthn key was used last time for 2 factor authentication and show it in user settings --- src/Entity/UserSystem/WebauthnKey.php | 2 +- .../WebauthnKeyLastUseTwoFactorProvider.php | 106 ++++++++++++++++++ templates/helper.twig | 10 +- templates/users/_2fa_settings.html.twig | 5 + templates/users/_api_tokens.html.twig | 14 +-- 5 files changed, 125 insertions(+), 12 deletions(-) create mode 100644 src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php diff --git a/src/Entity/UserSystem/WebauthnKey.php b/src/Entity/UserSystem/WebauthnKey.php index 5a6b7001..abb77a96 100644 --- a/src/Entity/UserSystem/WebauthnKey.php +++ b/src/Entity/UserSystem/WebauthnKey.php @@ -52,7 +52,7 @@ class WebauthnKey extends BasePublicKeyCredentialSource implements TimeStampable #[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)] protected ?\DateTimeInterface $last_time_used = null; - + public function getName(): string { return $this->name; diff --git a/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php b/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php new file mode 100644 index 00000000..5d67e36f --- /dev/null +++ b/src/Security/TwoFactor/WebauthnKeyLastUseTwoFactorProvider.php @@ -0,0 +1,106 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Security\TwoFactor; + +use App\Entity\UserSystem\WebauthnKey; +use Doctrine\ORM\EntityManagerInterface; +use Jbtronics\TFAWebauthn\Services\UserPublicKeyCredentialSourceRepository; +use Jbtronics\TFAWebauthn\Services\WebauthnProvider; +use Scheb\TwoFactorBundle\Security\TwoFactor\AuthenticationContextInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorFormRendererInterface; +use Scheb\TwoFactorBundle\Security\TwoFactor\Provider\TwoFactorProviderInterface; +use Symfony\Component\DependencyInjection\Attribute\AsDecorator; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\DependencyInjection\Attribute\AutowireDecorated; + +/** + * This class decorates the Webauthn TwoFactorProvider and adds additional logic which allows us to set a last used date + * on the used webauthn key, which can be viewed in the user settings. + */ +#[AsDecorator('jbtronics_webauthn_tfa.two_factor_provider')] +class WebauthnKeyLastUseTwoFactorProvider implements TwoFactorProviderInterface +{ + + public function __construct( + #[AutowireDecorated] + private TwoFactorProviderInterface $decorated, + private EntityManagerInterface $entityManager, + #[Autowire(service: 'jbtronics_webauthn_tfa.user_public_key_source_repo')] + private UserPublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository, + #[Autowire(service: 'jbtronics_webauthn_tfa.webauthn_provider')] + private WebauthnProvider $webauthnProvider, + ) + { + } + + public function beginAuthentication(AuthenticationContextInterface $context): bool + { + return $this->decorated->beginAuthentication($context); + } + + public function prepareAuthentication(object $user): void + { + $this->decorated->prepareAuthentication($user); + } + + public function validateAuthenticationCode(object $user, string $authenticationCode): bool + { + //Try to extract the used webauthn key from the code + $webauthnKey = $this->getWebauthnKeyFromCode($authenticationCode); + + //Perform the actual validation like normal + $tmp = $this->decorated->validateAuthenticationCode($user, $authenticationCode); + + //Update the last used date of the webauthn key, if the validation was successful + if($tmp && $webauthnKey !== null) { + $webauthnKey->updateLastTimeUsed(); + $this->entityManager->flush(); + } + + return $tmp; + } + + public function getFormRenderer(): TwoFactorFormRendererInterface + { + return $this->decorated->getFormRenderer(); + } + + private function getWebauthnKeyFromCode(string $authenticationCode): ?WebauthnKey + { + $publicKeyCredentialLoader = $this->webauthnProvider->getPublicKeyCredentialLoader(); + + //Try to load the public key credential from the code + $publicKeyCredential = $publicKeyCredentialLoader->load($authenticationCode); + + //Find the credential source for the given credential id + $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId($publicKeyCredential->rawId); + + //If the credential source is not an instance of WebauthnKey, return null + if(!($publicKeyCredentialSource instanceof WebauthnKey)) { + return null; + } + + return $publicKeyCredentialSource; + } +} \ No newline at end of file diff --git a/templates/helper.twig b/templates/helper.twig index f6de89d6..b6cf2dbe 100644 --- a/templates/helper.twig +++ b/templates/helper.twig @@ -230,4 +230,12 @@ {% endfor %} -{% endmacro parameters_table %} \ No newline at end of file +{% endmacro parameters_table %} + +{% macro format_date_nullable(datetime) %} + {% if datetime is null %} + {% trans %}datetime.never{% endtrans %} + {% else %} + {{ datetime|format_datetime }} + {% endif %} +{% endmacro %} \ No newline at end of file diff --git a/templates/users/_2fa_settings.html.twig b/templates/users/_2fa_settings.html.twig index 2b7f9c4c..80392c17 100644 --- a/templates/users/_2fa_settings.html.twig +++ b/templates/users/_2fa_settings.html.twig @@ -1,5 +1,7 @@ {# @var user \App\Entity\UserSystem\User #} +{% import "helper.twig" as helper %} +
@@ -124,6 +126,7 @@ # {% trans %}tfa_u2f.keys.name{% endtrans %} {% trans %}tfa_u2f.keys.added_date{% endtrans %} + {% trans %}api_tokens.last_time_used{% endtrans %} @@ -133,6 +136,7 @@ {{ loop.index }} (U2F) {{ key.name }} {{ key.addedDate | format_datetime }} + {# For legacy keys no last time use date is saved #} {% endfor %} @@ -141,6 +145,7 @@ {{ loop.index }} (WebAuthn) {{ key.name }} {{ key.addedDate | format_datetime }} + {{ helper.format_date_nullable(key.lastTimeUsed) }} {% endfor %} diff --git a/templates/users/_api_tokens.html.twig b/templates/users/_api_tokens.html.twig index de8771db..4c7c83e8 100644 --- a/templates/users/_api_tokens.html.twig +++ b/templates/users/_api_tokens.html.twig @@ -1,12 +1,6 @@ {# @var user \App\Entity\UserSystem\User #} -{% macro format_date(datetime) %} - {% if datetime is null %} - {% trans %}datetime.never{% endtrans %} - {% else %} - {{ datetime|format_datetime }} - {% endif %} -{% endmacro %} +{% import "helper.twig" as helper %}
@@ -48,15 +42,15 @@ {% trans%}api_token.ends_with{% endtrans%} ...{{ api_token.lastTokenChars }} {{ api_token.level.translationKey|trans }} - {{ _self.format_date(api_token.validUntil) }} + {{ helper.format_date_nullable(api_token.validUntil) }} {% if api_token.valid %} {% trans %}api_token.valid{% endtrans %} {% else %} {% trans %}api_token.expired{% endtrans %} {% endif %} - {{ _self.format_date(api_token.addedDate) }} - {{ _self.format_date(api_token.lastTimeUsed) }} + {{ helper.format_date_nullable(api_token.addedDate) }} + {{ helper.format_date_nullable(api_token.lastTimeUsed) }}