Added JSON login authenticator
This commit is contained in:
parent
7ef6a7ab03
commit
0fe5083a3e
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user