From d9c29986ac7385c7633f266124251a389994a3bd Mon Sep 17 00:00:00 2001
From: Oliver Bartsch <bo@cedev.de>
Date: Thu, 6 Jul 2023 14:15:56 +0200
Subject: [PATCH] [BUGFIX] Add webhook message for failed mfa attempts

In case a user did not pass necessary multi-factor
authentication, this is handled as a login failure.

Therefore, a webhook is now triggered for this
scenario, like done for login failures of the
first factor (usually username / password),
providing basic information about the user and
the used MFA provider.

Resolves: #101263
Related: #100129
Releases: main, 12.4
Change-Id: I56b79a74d291beb94d78bb42305dcfba86ecaa26
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/79825
Tested-by: Oliver Bartsch <bo@cedev.de>
Tested-by: core-ci <typo3@b13.com>
Tested-by: Benjamin Franzke <ben@bnf.dev>
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Benjamin Franzke <ben@bnf.dev>
---
 .../Classes/Controller/MfaController.php      |  2 +-
 .../Event/MfaVerificationFailedEvent.php      |  9 ++-
 .../MfaVerificationErrorOccurredMessage.php   | 67 +++++++++++++++++++
 .../Private/Language/locallang_db.xlf         |  3 +
 4 files changed, 79 insertions(+), 2 deletions(-)
 create mode 100644 typo3/sysext/webhooks/Classes/Message/MfaVerificationErrorOccurredMessage.php

diff --git a/typo3/sysext/backend/Classes/Controller/MfaController.php b/typo3/sysext/backend/Classes/Controller/MfaController.php
index adebc0f7c39f..0bf1ccd734e8 100644
--- a/typo3/sysext/backend/Classes/Controller/MfaController.php
+++ b/typo3/sysext/backend/Classes/Controller/MfaController.php
@@ -136,7 +136,7 @@ class MfaController extends AbstractMfaController
                 error: SystemLogErrorClassification::SECURITY_NOTICE
             );
             $this->eventDispatcher->dispatch(
-                new MfaVerificationFailedEvent($request, $propertyManager)
+                new MfaVerificationFailedEvent($request, $propertyManager, $mfaProvider)
             );
             // If failed, initiate a redirect back to the auth view
             return new RedirectResponse($this->uriBuilder->buildUriWithRedirect(
diff --git a/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php b/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php
index ebc3cf1a11e4..e2f0365fb727 100644
--- a/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php
+++ b/typo3/sysext/core/Classes/Authentication/Event/MfaVerificationFailedEvent.php
@@ -19,6 +19,7 @@ namespace TYPO3\CMS\Core\Authentication\Event;
 
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
+use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
 use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
 
 /**
@@ -29,6 +30,7 @@ final class MfaVerificationFailedEvent extends AbstractAuthenticationFailedEvent
     public function __construct(
         private readonly ServerRequestInterface $request,
         private readonly MfaProviderPropertyManager $propertyManager,
+        private readonly MfaProviderManifestInterface $mfaProvider,
     ) {
         parent::__construct($this->request);
     }
@@ -40,11 +42,16 @@ final class MfaVerificationFailedEvent extends AbstractAuthenticationFailedEvent
 
     public function getProviderIdentifier(): string
     {
-        return $this->propertyManager->getIdentifier();
+        return $this->mfaProvider->getIdentifier();
     }
 
     public function getProviderProperties(): array
     {
         return $this->propertyManager->getProperties();
     }
+
+    public function isProviderLocked(): bool
+    {
+        return $this->mfaProvider->isLocked($this->propertyManager);
+    }
 }
diff --git a/typo3/sysext/webhooks/Classes/Message/MfaVerificationErrorOccurredMessage.php b/typo3/sysext/webhooks/Classes/Message/MfaVerificationErrorOccurredMessage.php
new file mode 100644
index 000000000000..de5f1391195a
--- /dev/null
+++ b/typo3/sysext/webhooks/Classes/Message/MfaVerificationErrorOccurredMessage.php
@@ -0,0 +1,67 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+namespace TYPO3\CMS\Webhooks\Message;
+
+use Psr\Http\Message\UriInterface;
+use TYPO3\CMS\Core\Attribute\WebhookMessage;
+use TYPO3\CMS\Core\Authentication\Event\MfaVerificationFailedEvent;
+use TYPO3\CMS\Core\Messaging\WebhookMessageInterface;
+
+/**
+ * @internal not part of TYPO3's Core API
+ */
+#[WebhookMessage(
+    identifier: 'typo3/mfa-error',
+    description: 'LLL:EXT:webhooks/Resources/Private/Language/locallang_db.xlf:sys_webhook.webhook_type.typo3-mfa-error'
+)]
+final class MfaVerificationErrorOccurredMessage implements WebhookMessageInterface
+{
+    public function __construct(
+        private readonly bool $isFrontend,
+        private readonly UriInterface $url,
+        private readonly array $details,
+    ) {
+    }
+
+    public function jsonSerialize(): array
+    {
+        return [
+            'context' => $this->isFrontend ? 'frontend' : 'backend',
+            'url' => (string)$this->url,
+            'details' => $this->details,
+        ];
+    }
+
+    public static function createFromEvent(MfaVerificationFailedEvent $event): self
+    {
+        $user = $event->getUser();
+
+        return new self(
+            $event->isFrontendAttempt(),
+            $event->getRequest()->getUri(),
+            [
+                'user' => [
+                    'id' => $user->user[$user->userid_column],
+                    'name' => $user->user[$user->username_column],
+                ],
+                'provider' => $event->getProviderIdentifier(),
+                'isLocked' => $event->isProviderLocked(),
+            ]
+        );
+    }
+}
diff --git a/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf b/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf
index 1977525a2082..29f890cc3d96 100644
--- a/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf
+++ b/typo3/sysext/webhooks/Resources/Private/Language/locallang_db.xlf
@@ -73,6 +73,9 @@
 			<trans-unit id="sys_webhook.webhook_type.typo3-login-error" resname="sys_webhook.webhook_type.typo3-login-error">
 				<source>... when an error occurs on log in</source>
 			</trans-unit>
+			<trans-unit id="sys_webhook.webhook_type.typo3-mfa-error" resname="sys_webhook.webhook_type.typo3-mfa-error">
+				<source>... when an error occurs on multi-factor authentication</source>
+			</trans-unit>
 			<trans-unit id="sys_webhook.webhook_type.typo3-file-added" resname="sys_webhook.webhook_type.typo3-file-added">
 				<source>... when a file is added</source>
 			</trans-unit>
-- 
GitLab