Added JSON login authenticator

This commit is contained in:
Wouter de Jong 2020-04-05 13:12:09 +02:00
parent 7ef6a7ab03
commit 0fe5083a3e
4 changed files with 299 additions and 1 deletions

View File

@ -20,7 +20,7 @@ use Symfony\Component\DependencyInjection\Reference;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class JsonLoginFactory extends AbstractFactory
class JsonLoginFactory extends AbstractFactory implements AuthenticatorFactoryInterface
{
public function __construct()
{
@ -96,4 +96,18 @@ class JsonLoginFactory extends AbstractFactory
return $listenerId;
}
public function createAuthenticator(ContainerBuilder $container, string $id, array $config, string $userProviderId)
{
$authenticatorId = 'security.authenticator.json_login.'.$id;
$options = array_intersect_key($config, $this->options);
$container
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.json_login'))
->replaceArgument(1, new Reference($userProviderId))
->replaceArgument(2, isset($config['success_handler']) ? new Reference($this->createAuthenticationSuccessHandler($container, $id, $config)) : null)
->replaceArgument(3, isset($config['failure_handler']) ? new Reference($this->createAuthenticationFailureHandler($container, $id, $config)) : null)
->replaceArgument(4, $options);
return $authenticatorId;
}
}

View File

@ -98,6 +98,17 @@
<argument type="abstract">options</argument>
</service>
<service id="security.authenticator.json_login"
class="Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator"
abstract="true">
<argument type="service" id="security.http_utils" />
<argument type="abstract">user provider</argument>
<argument type="abstract">authentication success handler</argument>
<argument type="abstract">authentication failure handler</argument>
<argument type="abstract">options</argument>
<argument type="service" id="property_accessor" on-invalid="null" />
</service>
<service id="security.authenticator.anonymous"
class="Symfony\Component\Security\Http\Authenticator\AnonymousAuthenticator"
abstract="true">

View File

@ -0,0 +1,146 @@
<?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\Http\Authenticator;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\PropertyAccess\Exception\AccessException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\HttpUtils;
/**
* Provides a stateless implementation of an authentication via
* a JSON document composed of a username and a password.
*
* @author Kévin Dunglas <dunglas@gmail.com>
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
* @experimental in 5.1
*/
class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface
{
private $options;
private $httpUtils;
private $userProvider;
private $propertyAccessor;
private $successHandler;
private $failureHandler;
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null)
{
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
$this->httpUtils = $httpUtils;
$this->successHandler = $successHandler;
$this->failureHandler = $failureHandler;
$this->userProvider = $userProvider;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
public function supports(Request $request): ?bool
{
if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json')) {
return false;
}
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
return false;
}
return true;
}
public function getCredentials(Request $request)
{
$data = json_decode($request->getContent());
if (!$data instanceof \stdClass) {
throw new BadRequestHttpException('Invalid JSON.');
}
$credentials = [];
try {
$credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
if (!\is_string($credentials['username'])) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
}
if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
throw new BadCredentialsException('Invalid username.');
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
}
try {
$credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
if (!\is_string($credentials['password'])) {
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
}
} catch (AccessException $e) {
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
}
return $credentials;
}
public function getUser($credentials): ?UserInterface
{
return $this->userProvider->loadUserByUsername($credentials['username']);
}
public function getPassword($credentials): ?string
{
return $credentials['password'];
}
public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface
{
return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
{
if (null === $this->successHandler) {
return null; // let the original request continue
}
return $this->successHandler->onAuthenticationSuccess($request, $token);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if (null === $this->failureHandler) {
return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED);
}
return $this->failureHandler->onAuthenticationFailure($request, $exception);
}
public function isInteractive(): bool
{
return true;
}
}

View File

@ -0,0 +1,127 @@
<?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\Http\Tests\Authenticator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator;
use Symfony\Component\Security\Http\HttpUtils;
class JsonLoginAuthenticatorTest extends TestCase
{
private $userProvider;
/** @var JsonLoginAuthenticator */
private $authenticator;
protected function setUp(): void
{
$this->userProvider = $this->createMock(UserProviderInterface::class);
}
/**
* @dataProvider provideSupportData
*/
public function testSupport($request)
{
$this->setUpAuthenticator();
$this->assertTrue($this->authenticator->supports($request));
}
public function provideSupportData()
{
yield [new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}')];
$request = new Request([], [], [], [], [], [], '{"username": "dunglas", "password": "foo"}');
$request->setRequestFormat('json-ld');
yield [$request];
}
/**
* @dataProvider provideSupportsWithCheckPathData
*/
public function testSupportsWithCheckPath($request, $result)
{
$this->setUpAuthenticator(['check_path' => '/api/login']);
$this->assertSame($result, $this->authenticator->supports($request));
}
public function provideSupportsWithCheckPathData()
{
yield [Request::create('/api/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), true];
yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false];
}
public function testGetCredentials()
{
$this->setUpAuthenticator();
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}');
$this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request));
}
public function testGetCredentialsCustomPath()
{
$this->setUpAuthenticator([
'username_path' => 'authentication.username',
'password_path' => 'authentication.password',
]);
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}');
$this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request));
}
/**
* @dataProvider provideInvalidGetCredentialsData
*/
public function testGetCredentialsInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class)
{
$this->expectException($exceptionType);
$this->expectExceptionMessage($errorMessage);
$this->setUpAuthenticator();
$this->authenticator->getCredentials($request);
}
public function provideInvalidGetCredentialsData()
{
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']);
yield [$request, 'Invalid JSON.'];
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"usr": "dunglas", "password": "foo"}');
yield [$request, 'The key "username" must be provided'];
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "pass": "foo"}');
yield [$request, 'The key "password" must be provided'];
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": 1, "password": "foo"}');
yield [$request, 'The key "username" must be a string.'];
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": 1}');
yield [$request, 'The key "password" must be a string.'];
$username = str_repeat('x', Security::MAX_USERNAME_LENGTH + 1);
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], sprintf('{"username": "%s", "password": 1}', $username));
yield [$request, 'Invalid username.', BadCredentialsException::class];
}
private function setUpAuthenticator(array $options = [])
{
$this->authenticator = new JsonLoginAuthenticator(new HttpUtils(), $this->userProvider, null, null, $options);
}
}