From 62c950bee3c1708aaebd5ff5a2be5dc89c32d0f2 Mon Sep 17 00:00:00 2001 From: 6801318d8d <144167388+6801318d8d@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:32:59 +0100 Subject: [PATCH] feat(webhook_listeners): make failed login events webhook compatible AnyLoginFailedEvent and TwoFactorProviderChallengeFailed now implement IWebhookCompatibleEvent, so admins can register webhooks that fire when a login fails (wrong password or unknown login name, or a failed second factor). The AnyLoginFailedEvent payload only contains the login name: the attempted password is never serialized. TwoFactorProviderChallengeFailed sends the user and the provider id and display name. Signed-off-by: 6801318d8d <144167388+6801318d8d@users.noreply.github.com> Assisted-by: ClaudeCode:claude-opus-5-5 --- .../Events/AnyLoginFailedEvent.php | 16 +++++++- .../TwoFactorProviderChallengeFailed.php | 19 ++++++++- .../Events/AnyLoginFailedEventTest.php | 29 +++++++++++++ .../TwoFactorProviderChallengeFailedTest.php | 41 +++++++++++++++++++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 tests/lib/Authentication/Events/AnyLoginFailedEventTest.php create mode 100644 tests/lib/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailedTest.php diff --git a/lib/public/Authentication/Events/AnyLoginFailedEvent.php b/lib/public/Authentication/Events/AnyLoginFailedEvent.php index cea2a7f59d984..0befc6c6981a5 100644 --- a/lib/public/Authentication/Events/AnyLoginFailedEvent.php +++ b/lib/public/Authentication/Events/AnyLoginFailedEvent.php @@ -10,13 +10,15 @@ namespace OCP\Authentication\Events; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IWebhookCompatibleEvent; /** * Emitted when the authentication fails * * @since 26.0.0 + * @since 36.0.0 Implements IWebhookCompatibleEvent */ -class AnyLoginFailedEvent extends Event { +class AnyLoginFailedEvent extends Event implements IWebhookCompatibleEvent { private string $loginName; private ?string $password; @@ -51,4 +53,16 @@ public function getLoginName(): string { public function getPassword(): ?string { return $this->password; } + + /** + * The attempted password is never part of the payload. + * + * @since 36.0.0 + */ + #[\Override] + public function getWebhookSerializable(): array { + return [ + 'loginName' => $this->loginName, + ]; + } } diff --git a/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php b/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php index bcabb3687f208..53f556412f189 100644 --- a/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php +++ b/lib/public/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailed.php @@ -10,12 +10,15 @@ namespace OCP\Authentication\TwoFactorAuth; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IWebhookCompatibleEvent; +use OCP\EventDispatcher\JsonSerializer; use OCP\IUser; /** * @since 28.0.0 + * @since 36.0.0 Implements IWebhookCompatibleEvent */ -class TwoFactorProviderChallengeFailed extends Event { +class TwoFactorProviderChallengeFailed extends Event implements IWebhookCompatibleEvent { /** * @since 28.0.0 */ @@ -39,4 +42,18 @@ public function getUser(): IUser { public function getProvider(): IProvider { return $this->provider; } + + /** + * @since 36.0.0 + */ + #[\Override] + public function getWebhookSerializable(): array { + return [ + 'user' => JsonSerializer::serializeUser($this->user), + 'provider' => [ + 'id' => $this->provider->getId(), + 'displayName' => $this->provider->getDisplayName(), + ], + ]; + } } diff --git a/tests/lib/Authentication/Events/AnyLoginFailedEventTest.php b/tests/lib/Authentication/Events/AnyLoginFailedEventTest.php new file mode 100644 index 0000000000000..8e1d80b05b7ae --- /dev/null +++ b/tests/lib/Authentication/Events/AnyLoginFailedEventTest.php @@ -0,0 +1,29 @@ +assertInstanceOf(IWebhookCompatibleEvent::class, $event); + $this->assertSame(['loginName' => 'alice'], $event->getWebhookSerializable()); + } + + public function testWebhookSerializableNeverContainsPassword(): void { + $event = new AnyLoginFailedEvent('alice', 'secret-password'); + + $this->assertStringNotContainsString('secret-password', json_encode($event->getWebhookSerializable(), JSON_THROW_ON_ERROR)); + } +} diff --git a/tests/lib/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailedTest.php b/tests/lib/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailedTest.php new file mode 100644 index 0000000000000..dc4bae154ea55 --- /dev/null +++ b/tests/lib/Authentication/TwoFactorAuth/TwoFactorProviderChallengeFailedTest.php @@ -0,0 +1,41 @@ +createMock(IUser::class); + $user->method('getUID')->willReturn('alice'); + $user->method('getDisplayName')->willReturn('Alice'); + $provider = $this->createMock(IProvider::class); + $provider->method('getId')->willReturn('totp'); + $provider->method('getDisplayName')->willReturn('TOTP (Authenticator app)'); + + $event = new TwoFactorProviderChallengeFailed($user, $provider); + + $this->assertInstanceOf(IWebhookCompatibleEvent::class, $event); + $this->assertSame([ + 'user' => [ + 'uid' => 'alice', + 'displayName' => 'Alice', + ], + 'provider' => [ + 'id' => 'totp', + 'displayName' => 'TOTP (Authenticator app)', + ], + ], $event->getWebhookSerializable()); + } +}