Skip to content

Commit bdadfe2

Browse files
committed
Added JSON login authenticator
1 parent 7998121 commit bdadfe2

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Authenticator;
13+
14+
use Symfony\Component\HttpFoundation\JsonResponse;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
18+
use Symfony\Component\PropertyAccess\Exception\AccessException;
19+
use Symfony\Component\PropertyAccess\PropertyAccess;
20+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
21+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
22+
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
23+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
24+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
25+
use Symfony\Component\Security\Core\Security;
26+
use Symfony\Component\Security\Core\User\UserInterface;
27+
use Symfony\Component\Security\Core\User\UserProviderInterface;
28+
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
29+
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
30+
use Symfony\Component\Security\Http\HttpUtils;
31+
32+
/**
33+
* Provides a stateless implementation of an authentication via
34+
* a JSON document composed of a username and a password.
35+
*
36+
* @author Kévin Dunglas <dunglas@gmail.com>
37+
* @author Wouter de Jong <wouter@wouterj.nl>
38+
*
39+
* @final
40+
* @experimental in 5.1
41+
*/
42+
class JsonLoginAuthenticator implements InteractiveAuthenticatorInterface, PasswordAuthenticatedInterface
43+
{
44+
private $options;
45+
private $httpUtils;
46+
private $userProvider;
47+
private $propertyAccessor;
48+
private $successHandler;
49+
private $failureHandler;
50+
51+
public function __construct(HttpUtils $httpUtils, UserProviderInterface $userProvider, ?AuthenticationSuccessHandlerInterface $successHandler = null, ?AuthenticationFailureHandlerInterface $failureHandler = null, array $options = [], ?PropertyAccessorInterface $propertyAccessor = null)
52+
{
53+
$this->options = array_merge(['username_path' => 'username', 'password_path' => 'password'], $options);
54+
$this->httpUtils = $httpUtils;
55+
$this->successHandler = $successHandler;
56+
$this->failureHandler = $failureHandler;
57+
$this->userProvider = $userProvider;
58+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
59+
}
60+
61+
public function supports(Request $request): ?bool
62+
{
63+
if (false === strpos($request->getRequestFormat(), 'json') && false === strpos($request->getContentType(), 'json')) {
64+
return false;
65+
}
66+
67+
if (isset($this->options['check_path']) && !$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
68+
return false;
69+
}
70+
71+
return true;
72+
}
73+
74+
public function getCredentials(Request $request)
75+
{
76+
$data = json_decode($request->getContent());
77+
if (!$data instanceof \stdClass) {
78+
throw new BadRequestHttpException('Invalid JSON.');
79+
}
80+
81+
$credentials = [];
82+
try {
83+
$credentials['username'] = $this->propertyAccessor->getValue($data, $this->options['username_path']);
84+
85+
if (!\is_string($credentials['username'])) {
86+
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['username_path']));
87+
}
88+
89+
if (\strlen($credentials['username']) > Security::MAX_USERNAME_LENGTH) {
90+
throw new BadCredentialsException('Invalid username.');
91+
}
92+
} catch (AccessException $e) {
93+
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['username_path']), $e);
94+
}
95+
96+
try {
97+
$credentials['password'] = $this->propertyAccessor->getValue($data, $this->options['password_path']);
98+
99+
if (!\is_string($credentials['password'])) {
100+
throw new BadRequestHttpException(sprintf('The key "%s" must be a string.', $this->options['password_path']));
101+
}
102+
} catch (AccessException $e) {
103+
throw new BadRequestHttpException(sprintf('The key "%s" must be provided.', $this->options['password_path']), $e);
104+
}
105+
106+
return $credentials;
107+
}
108+
109+
public function getUser($credentials): ?UserInterface
110+
{
111+
return $this->userProvider->loadUserByUsername($credentials['username']);
112+
}
113+
114+
public function getPassword($credentials): ?string
115+
{
116+
return $credentials['password'];
117+
}
118+
119+
public function createAuthenticatedToken(UserInterface $user, string $providerKey): TokenInterface
120+
{
121+
return new UsernamePasswordToken($user, null, $providerKey, $user->getRoles());
122+
}
123+
124+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey): ?Response
125+
{
126+
if (null === $this->successHandler) {
127+
return null; // let the original request continue
128+
}
129+
130+
return $this->successHandler->onAuthenticationSuccess($request, $token);
131+
}
132+
133+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
134+
{
135+
if (null === $this->failureHandler) {
136+
return new JsonResponse(['error' => $exception->getMessageKey()], JsonResponse::HTTP_UNAUTHORIZED);
137+
}
138+
139+
return $this->failureHandler->onAuthenticationFailure($request, $exception);
140+
}
141+
142+
public function isInteractive(): bool
143+
{
144+
return true;
145+
}
146+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Tests\Authenticator;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
17+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
18+
use Symfony\Component\Security\Core\Security;
19+
use Symfony\Component\Security\Core\User\UserProviderInterface;
20+
use Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator;
21+
use Symfony\Component\Security\Http\HttpUtils;
22+
23+
class JsonLoginAuthenticatorTest extends TestCase
24+
{
25+
private $userProvider;
26+
/** @var JsonLoginAuthenticator */
27+
private $authenticator;
28+
29+
protected function setUp(): void
30+
{
31+
$this->userProvider = $this->createMock(UserProviderInterface::class);
32+
}
33+
34+
/**
35+
* @dataProvider provideSupportData
36+
*/
37+
public function testSupport($request)
38+
{
39+
$this->setUpAuthenticator();
40+
41+
$this->assertTrue($this->authenticator->supports($request));
42+
}
43+
44+
public function provideSupportData()
45+
{
46+
yield [new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}')];
47+
48+
$request = new Request([], [], [], [], [], [], '{"username": "dunglas", "password": "foo"}');
49+
$request->setRequestFormat('json-ld');
50+
yield [$request];
51+
}
52+
53+
/**
54+
* @dataProvider provideSupportsWithCheckPathData
55+
*/
56+
public function testSupportsWithCheckPath($request, $result)
57+
{
58+
$this->setUpAuthenticator(['check_path' => '/api/login']);
59+
60+
$this->assertSame($result, $this->authenticator->supports($request));
61+
}
62+
63+
public function provideSupportsWithCheckPathData()
64+
{
65+
yield [Request::create('/api/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), true];
66+
yield [Request::create('/login', 'GET', [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']), false];
67+
}
68+
69+
public function testGetCredentials()
70+
{
71+
$this->setUpAuthenticator();
72+
73+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": "foo"}');
74+
$this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request));
75+
}
76+
77+
public function testGetCredentialsCustomPath()
78+
{
79+
$this->setUpAuthenticator([
80+
'username_path' => 'authentication.username',
81+
'password_path' => 'authentication.password',
82+
]);
83+
84+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"authentication": {"username": "dunglas", "password": "foo"}}');
85+
$this->assertEquals(['username' => 'dunglas', 'password' => 'foo'], $this->authenticator->getCredentials($request));
86+
}
87+
88+
/**
89+
* @dataProvider provideInvalidGetCredentialsData
90+
*/
91+
public function testGetCredentialsInvalid($request, $errorMessage, $exceptionType = BadRequestHttpException::class)
92+
{
93+
$this->expectException($exceptionType);
94+
$this->expectExceptionMessage($errorMessage);
95+
96+
$this->setUpAuthenticator();
97+
98+
$this->authenticator->getCredentials($request);
99+
}
100+
101+
public function provideInvalidGetCredentialsData()
102+
{
103+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json']);
104+
yield [$request, 'Invalid JSON.'];
105+
106+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"usr": "dunglas", "password": "foo"}');
107+
yield [$request, 'The key "username" must be provided'];
108+
109+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "pass": "foo"}');
110+
yield [$request, 'The key "password" must be provided'];
111+
112+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": 1, "password": "foo"}');
113+
yield [$request, 'The key "username" must be a string.'];
114+
115+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], '{"username": "dunglas", "password": 1}');
116+
yield [$request, 'The key "password" must be a string.'];
117+
118+
$username = str_repeat('x', Security::MAX_USERNAME_LENGTH + 1);
119+
$request = new Request([], [], [], [], [], ['HTTP_CONTENT_TYPE' => 'application/json'], sprintf('{"username": "%s", "password": 1}', $username));
120+
yield [$request, 'Invalid username.', BadCredentialsException::class];
121+
}
122+
123+
private function setUpAuthenticator(array $options = [])
124+
{
125+
$this->authenticator = new JsonLoginAuthenticator(new HttpUtils(), $this->userProvider, null, null, $options);
126+
}
127+
}

0 commit comments

Comments
 (0)