Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

namespace Ibexa\Bundle\User\Security\Authentication;

use Ibexa\Bundle\User\Security\Exception\BlankCredentialsException;
use Ibexa\Contracts\Core\Repository\Exceptions\PasswordInUnsupportedFormatException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -17,6 +18,10 @@

final class DefaultAuthenticationFailureHandler extends HttpDefaultAuthenticationFailureHandler
{
private const string USERNAME_PARAMETER = '_username';

private const string PASSWORD_PARAMETER = '_password';

#[\Override]
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
{
Expand All @@ -30,14 +35,40 @@ public function onAuthenticationFailure(Request $request, AuthenticationExceptio
if ($exception instanceof BadCredentialsException) {
$previous = $exception->getPrevious();
$code = $previous ? $previous->getCode() : 0;
$blankFields = $this->getBlankCredentialFields($request);

$exception = new BadCredentialsException(
'Bad credentials.',
$code,
$previous
);
$exception = $blankFields === []
? new BadCredentialsException(
'Bad credentials.',
$code,
$previous
)
: new BlankCredentialsException(
$blankFields,
'Bad credentials.',
$code,
$previous
);
}

return parent::onAuthenticationFailure($request, $exception);
}

/**
* @return list<BlankCredentialsException::FIELD_*>
*/
private function getBlankCredentialFields(Request $request): array
{
$blankFields = [];

if (trim((string)$request->request->get(self::USERNAME_PARAMETER, '')) === '') {
$blankFields[] = BlankCredentialsException::FIELD_USERNAME;
}

if ((string)$request->request->get(self::PASSWORD_PARAMETER, '') === '') {
$blankFields[] = BlankCredentialsException::FIELD_PASSWORD;
}

return $blankFields;
}
}
62 changes: 62 additions & 0 deletions src/bundle/Security/Exception/BlankCredentialsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\User\Security\Exception;

use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Throwable;

final class BlankCredentialsException extends BadCredentialsException
{
public const string FIELD_USERNAME = 'username';

public const string FIELD_PASSWORD = 'password';

/** @var list<self::FIELD_*> */
private array $blankFields;

/**
* @param list<self::FIELD_*> $blankFields
*/
public function __construct(
array $blankFields,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To simplify it, that could be public readonly constructor promoted property (to reduce code and get rid of its getter).

string $message = '',
int $code = 0,
?Throwable $previous = null
) {
parent::__construct($message, $code, $previous);

$this->blankFields = $blankFields;
}

/**
* @return list<self::FIELD_*>
*/
public function getBlankFields(): array
{
return $this->blankFields;
}

/**
* @return array{list<self::FIELD_*>, array<mixed>}
*/
public function __serialize(): array
{
return [$this->blankFields, parent::__serialize()];
}

/**
* @param array{list<self::FIELD_*>, array<mixed>} $data
*/
public function __unserialize(array $data): void
{
[$this->blankFields, $parentData] = $data;

parent::__unserialize($parentData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Ibexa\Tests\Bundle\User\Security\Authentication;

use Ibexa\Bundle\User\Security\Authentication\DefaultAuthenticationFailureHandler;
use Ibexa\Bundle\User\Security\Exception\BlankCredentialsException;
use Ibexa\Contracts\Core\Repository\Exceptions\PasswordInUnsupportedFormatException;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -59,13 +60,14 @@ public function testOnAuthenticationFailureAltersBadCredentialsExceptionMessage(
'_security.last_error',
self::callback(static function (AuthenticationException $exception): bool {
self::assertInstanceOf(BadCredentialsException::class, $exception);
self::assertNotInstanceOf(BlankCredentialsException::class, $exception);
self::assertSame('Bad credentials.', $exception->getMessage());

return true;
})
);

$request = $this->getRequest($session);
$request = $this->getRequest($session, 'admin', 'secret');
$originalException = new BadCredentialsException('Original message');

$this->httpUtils
Expand All @@ -76,9 +78,100 @@ public function testOnAuthenticationFailureAltersBadCredentialsExceptionMessage(
$this->handler->onAuthenticationFailure($request, $originalException);
}

private function getRequest(?Session $session = null): Request
/**
* @dataProvider dataProviderForBlankCredentials
*
* @param list<BlankCredentialsException::FIELD_*> $expectedBlankFields
*/
public function testOnAuthenticationFailureReportsBlankCredentialFields(
string $username,
string $password,
array $expectedBlankFields
): void {
$session = $this->getSession();
$session
->expects(self::once())
->method('set')
->with(
'_security.last_error',
self::callback(
static function (AuthenticationException $exception) use ($expectedBlankFields): bool {
self::assertInstanceOf(BlankCredentialsException::class, $exception);
self::assertSame($expectedBlankFields, $exception->getBlankFields());
self::assertSame('Bad credentials.', $exception->getMessage());

return true;
}
)
);

$request = $this->getRequest($session, $username, $password);

$this->handler->onAuthenticationFailure($request, new BadCredentialsException('Original message'));
}

public function testOnAuthenticationFailureTreatsWhitespacePasswordAsFilled(): void
{
$request = new Request();
$session = $this->getSession();
$session
->expects(self::once())
->method('set')
->with(
'_security.last_error',
self::callback(static function (AuthenticationException $exception): bool {
self::assertNotInstanceOf(BlankCredentialsException::class, $exception);

return true;
})
);

$request = $this->getRequest($session, 'admin', ' ');

$this->handler->onAuthenticationFailure($request, new BadCredentialsException('Original message'));
}

/**
* @return array<string, array{0: string, 1: string, 2: list<BlankCredentialsException::FIELD_*>}>
*/
public function dataProviderForBlankCredentials(): array
{
return [
'both fields blank' => [
'',
'',
[BlankCredentialsException::FIELD_USERNAME, BlankCredentialsException::FIELD_PASSWORD],
],
'blank username only' => [
'',
'secret',
[BlankCredentialsException::FIELD_USERNAME],
],
'blank password only' => [
'admin',
'',
[BlankCredentialsException::FIELD_PASSWORD],
],
'whitespace-only username' => [
' ',
'secret',
[BlankCredentialsException::FIELD_USERNAME],
],
];
}

private function getRequest(?Session $session = null, ?string $username = null, ?string $password = null): Request
{
$parameters = [];

if ($username !== null) {
$parameters['_username'] = $username;
}

if ($password !== null) {
$parameters['_password'] = $password;
}

$request = new Request([], $parameters);
$request->setSession($session ?? $this->getSession());

return $request;
Expand Down
Loading