Skip to content

Commit

Permalink
Created HttpBasicAuthenticator and some Guard traits
Browse files Browse the repository at this point in the history
  • Loading branch information
wouterj committed Apr 20, 2020
1 parent c321f4d commit a6890db
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 1 deletion.
@@ -0,0 +1,91 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authentication\Authenticator;

use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AuthenticatorInterface;

/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class HttpBasicAuthenticator implements AuthenticatorInterface
{
use UserProviderTrait, UsernamePasswordTrait {
UserProviderTrait::getUser as getUserTrait;
}

private $realmName;
private $userProvider;
private $encoderFactory;
private $logger;

public function __construct(string $realmName, UserProviderInterface $userProvider, EncoderFactoryInterface $encoderFactory, ?LoggerInterface $logger = null)
{
$this->realmName = $realmName;
$this->userProvider = $userProvider;
$this->encoderFactory = $encoderFactory;
$this->logger = $logger;
}

public function start(Request $request, AuthenticationException $authException = null)
{
$response = new Response();
$response->headers->set('WWW-Authenticate', sprintf('Basic realm="%s"', $this->realmName));
$response->setStatusCode(401);

return $response;
}

public function supports(Request $request): bool
{
return $request->headers->has('PHP_AUTH_USER');
}

public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
return $this->getUserTrait($credentials, $this->userProvider);
}

public function getCredentials(Request $request)
{
return [
'username' => $request->headers->get('PHP_AUTH_USER'),
'password' => $request->headers->get('PHP_AUTH_PW', ''),
];
}

public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey): ?Response
{
return null;
}

public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null !== $this->logger) {
$this->logger->info('Basic authentication failed for user.', ['username' => $request->headers->get('PHP_AUTH_USER'), 'exception' => $exception]);
}

return $this->start($request, $exception);
}

public function supportsRememberMe(): bool
{
return false;
}
}
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authentication\Authenticator;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
* @author Wouter de Jong <wouter@wouterj.nl>
*/
trait UserProviderTrait
{
public function getUser($credentials, UserProviderInterface $userProvider): ?UserInterface
{
return $userProvider->loadUserByUsername($credentials['username']);
}
}
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Security\Core\Authentication\Authenticator;

use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;

/**
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @property EncoderFactoryInterface $encoderFactory
*/
trait UsernamePasswordTrait
{
public function checkCredentials($credentials, UserInterface $user): bool
{
if (!$this->encoderFactory instanceof EncoderFactoryInterface) {
throw new \LogicException(\get_class($this).' uses the '.__CLASS__.' trait, which requires an $encoderFactory property to be initialized with an '.EncoderFactoryInterface::class.' implementation.');
}

if ('' === $credentials['password']) {
throw new BadCredentialsException('The presented password cannot be empty.');
}

if (!$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $credentials['password'], null)) {
throw new BadCredentialsException('The presented password is invalid.');
}

return true;
}

public function createAuthenticatedToken(UserInterface $user, $providerKey): GuardTokenInterface
{
return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
}
}
Expand Up @@ -12,13 +12,14 @@
namespace Symfony\Component\Security\Core\Authentication\Token;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Guard\Token\GuardTokenInterface;

/**
* UsernamePasswordToken implements a username and password token.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class UsernamePasswordToken extends AbstractToken
class UsernamePasswordToken extends AbstractToken implements GuardTokenInterface
{
private $credentials;
private $providerKey;
Expand Down
@@ -0,0 +1,114 @@
<?php

namespace Symfony\Component\Security\Core\Tests\Authentication\Authenticator;

use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Authenticator\HttpBasicAuthenticator;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\Encoder\PasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class HttpBasicAuthenticatorTest extends TestCase
{
/** @var UserProviderInterface|MockObject */
private $userProvider;
/** @var EncoderFactoryInterface|MockObject */
private $encoderFactory;
/** @var PasswordEncoderInterface|MockObject */
private $encoder;

protected function setUp(): void
{
$this->userProvider = $this->getMockBuilder(UserProviderInterface::class)->getMock();
$this->encoderFactory = $this->getMockBuilder(EncoderFactoryInterface::class)->getMock();
$this->encoder = $this->getMockBuilder(PasswordEncoderInterface::class)->getMock();
$this->encoderFactory
->expects($this->any())
->method('getEncoder')
->willReturn($this->encoder);
}

public function testValidUsernameAndPasswordServerParameters()
{
$request = new Request([], [], [], [], [], [
'PHP_AUTH_USER' => 'TheUsername',
'PHP_AUTH_PW' => 'ThePassword',
]);

$guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);
$credentials = $guard->getCredentials($request);
$this->assertEquals([
'username' => 'TheUsername',
'password' => 'ThePassword',
], $credentials);

$mockedUser = $this->getMockBuilder(UserInterface::class)->getMock();
$mockedUser->expects($this->any())->method('getPassword')->willReturn('ThePassword');

$this->userProvider
->expects($this->any())
->method('loadUserByUsername')
->with('TheUsername')
->willReturn($mockedUser);

$user = $guard->getUser($credentials, $this->userProvider);
$this->assertSame($mockedUser, $user);

$this->encoder
->expects($this->any())
->method('isPasswordValid')
->with('ThePassword', 'ThePassword', null)
->willReturn(true);

$checkCredentials = $guard->checkCredentials($credentials, $user);
$this->assertTrue($checkCredentials);
}

/** @dataProvider provideInvalidPasswords */
public function testInvalidPassword($presentedPassword, $exceptionMessage)
{
$guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);

$this->encoder
->expects($this->any())
->method('isPasswordValid')
->willReturn(false);

$this->expectException(BadCredentialsException::class);
$this->expectExceptionMessage($exceptionMessage);

$guard->checkCredentials([
'username' => 'TheUsername',
'password' => $presentedPassword,
], $this->getMockBuilder(UserInterface::class)->getMock());
}

public function provideInvalidPasswords()
{
return [
['InvalidPassword', 'The presented password is invalid.'],
['', 'The presented password cannot be empty.'],
];
}

/** @dataProvider provideMissingHttpBasicServerParameters */
public function testHttpBasicServerParametersMissing(array $serverParameters)
{
$request = new Request([], [], [], [], [], $serverParameters);

$guard = new HttpBasicAuthenticator('test', $this->userProvider, $this->encoderFactory);
$this->assertFalse($guard->supports($request));
}

public function provideMissingHttpBasicServerParameters()
{
return [
[[]],
[['PHP_AUTH_PW' => 'ThePassword']],
];
}
}

0 comments on commit a6890db

Please sign in to comment.