Skip to content

Commit

Permalink
Fixing & adding tests
Browse files Browse the repository at this point in the history
  • Loading branch information
TristanPouliquen committed Jun 9, 2021
1 parent 383313c commit 656812a
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 29 deletions.
13 changes: 11 additions & 2 deletions Security/Authenticator/JWTTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken;
Expand Down Expand Up @@ -93,8 +94,16 @@ public function authenticate(Request $request): PassportInterface
{
$tokenExtractor = $this->getTokenExtractor();

if (!$this->payload = $this->jwtManager->decodeFromJsonWebToken($tokenExtractor->extract($request))) {
throw new InvalidTokenException('Invalid JWT Token');
try {
if (!$this->payload = $this->jwtManager->decodeFromJsonWebToken($tokenExtractor->extract($request))) {
throw new InvalidTokenException('Invalid JWT Token');
}
} catch (JWTDecodeFailureException $e) {
if (JWTDecodeFailureException::EXPIRED_TOKEN === $e->getReason()) {
throw new ExpiredTokenException();
}

throw new InvalidTokenException('Invalid JWT Token', 0, $e);
}

$idClaim = $this->jwtManager->getUserIdClaim();
Expand Down
2 changes: 1 addition & 1 deletion Security/User/JWTUserProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function __construct(string $class)
*
* @param array $payload The JWT payload from which to create an instance
*/
public function loadUserByUsername(string $username, array $payload = []): UserInterface
public function loadUserByUsername($username, array $payload = [])
{
return $this->loadUserByUsernameAndPayload($username, $payload);
}
Expand Down
3 changes: 3 additions & 0 deletions Services/JWTTokenManagerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Lexik\Bundle\JWTAuthenticationBundle\Services;

use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\User\UserInterface;

Expand All @@ -23,11 +24,13 @@ public function create(UserInterface $user);

/**
* @return array|false The JWT token payload or false if an error occurs
* @throws JWTDecodeFailureException
*/
public function decode(TokenInterface $token);

/**
* @return array|false The JWT token payload or false if an error occurs
* @throws JWTDecodeFailureException
*/
public function decodeFromJsonWebToken(string $jwtToken);

Expand Down
7 changes: 6 additions & 1 deletion Tests/Functional/CompleteTokenAuthenticationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Lexik\Bundle\JWTAuthenticationBundle\Tests\Stubs\JWTUser;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\User\InMemoryUser;
use Symfony\Component\Security\Core\User\User;

/**
Expand Down Expand Up @@ -33,7 +34,11 @@ public function testAccessSecuredRoute()

if ('lexik_jwt' === static::$kernel->getUserProvider()) {
$this->assertSame(JWTUser::class, $content['class']);
} else {
} else if (class_exists(InMemoryUser::class)) {
// This class has been introduced in SF5.3, for better security code readability
// See https://symfony.com/blog/new-in-symfony-5-3-improvements-for-security-users
$this->assertSame(InMemoryUser::class, $content['class']);
}else {
$this->assertSame(User::class, $content['class']);
}

Expand Down
131 changes: 116 additions & 15 deletions Tests/Security/Authenticator/JWTTokenAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,152 @@

namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Security\Authenticator;

use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationFailureEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTInvalidEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTNotFoundEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Events;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\ExpiredTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidPayloadException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\JWTTokenAuthenticator;
use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Tests\Stubs\User as AdvancedUserStub;
use Lexik\Bundle\JWTAuthenticationBundle\TokenExtractor\TokenExtractorInterface;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/** @requires PHP >= 7.2 */
class JWTTokenAuthenticatorTest extends TestCase
{
public function testAuthenticate() {
$jwtManager = $this->getJWTManagerMock();
$userIdClaim = 'sub';
$payload = [$userIdClaim => 'lexik'];
$rawToken = 'token';
$userRoles = ['ROLE_USER'];

$userStub = new AdvancedUserStub('lexik', 'password', 'user@gmail.com', $userRoles);

$jwtManager = $this->getJWTManagerMock(null, $userIdClaim);
$jwtManager
->expects($this->once())
->method('decodeFromJsonWebToken')
->willReturn(['username' => 'lexik']);

$userProvider = $this->getUserProviderMock();
$userProvider
->expects($this->once())
->method('loadUserByIdentifier')
->with($payload['sub'])
->willReturn($userStub);

$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock($rawToken),
$userProvider
);

$this->assertSame($userStub, ($authenticator->authenticate($this->getRequestMock()))->getUser());
}

public function testAuthenticateWithExpiredTokenThrowsException() {
$jwtManager = $this->getJWTManagerMock();
$jwtManager->method('decodeFromJsonWebToken')
->will($this->throwException(new JWTDecodeFailureException(JWTDecodeFailureException::EXPIRED_TOKEN, 'Expired JWT Token')));

$this->expectException(ExpiredTokenException::class);

$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token'),
$this->getUserProviderMock()
);

$authenticator->authenticate($this->getRequestMock());
}

public function testAuthenticateWithInvalidTokenThrowsException() {
$jwtManager = $this->getJWTManagerMock();
$jwtManager->method('decodeFromJsonWebToken')
->willThrowException(new JWTDecodeFailureException(
JWTDecodeFailureException::INVALID_TOKEN,
'Invalid JWT Token')
);
$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token'),
$this->getUserProviderMock()
);

$this->expectException(InvalidTokenException::class);

$authenticator->authenticate($this->getRequestMock());
}

public function testOnAuthenticationFailureWithInvalidToken()
{
public function testAuthenticateWithUndecodableTokenThrowsException() {
$jwtManager = $this->getJWTManagerMock();
$jwtManager->method('decodeFromJsonWebToken')
->willReturn(null);
$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token'),
$this->getUserProviderMock()
);

$this->expectException(InvalidTokenException::class);

$authenticator->authenticate($this->getRequestMock());
}

public function testAuthenticationWithInvalidPayloadThrowsException() {
$jwtManager = $this->getJWTManagerMock();
$jwtManager->method('decodeFromJsonWebToken')
->willReturn(['foo' => 'bar']);
$jwtManager->method('getUserIdClaim')
->willReturn('identifier');
$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token'),
$this->getUserProviderMock()
);

$this->expectException(InvalidPayloadException::class);

$authenticator->authenticate($this->getRequestMock());
}

public function testAuthenticateWithInvalidUserThrowsException() {
$jwtManager = $this->getJWTManagerMock();
$jwtManager->method('decodeFromJsonWebToken')
->willReturn(['identifier' => 'bar']);
$jwtManager->method('getUserIdClaim')
->willReturn('identifier');

$userProvider = $this->getUserProviderMock();
$userProvider->method('loadUserByIdentifierAndPayload')
->willThrowException(new UserNotFoundException());
$authenticator = new JWTTokenAuthenticator(
$jwtManager,
$this->getEventDispatcherMock(),
$this->getTokenExtractorMock('token'),
$this->getUserProviderMock()
);

$this->expectException(UserNotFoundException::class);

$authenticator->authenticate($this->getRequestMock());
}

public function testOnAuthenticationFailureWithInvalidToken() {
$authException = new InvalidTokenException();
$expectedResponse = new JWTAuthenticationFailureResponse('Invalid JWT Token');

Expand Down Expand Up @@ -137,16 +248,6 @@ private function getUserProviderMock()
->getMock();
}

/**
* @return \PHPUnit\Framework\MockObject\MockObject|TokenStorageInterface
*/
private function getTokenStorageMock()
{
return $this->getMockBuilder(TokenStorageInterface::class)
->setMethods(['getToken', 'setToken'])
->getMockForAbstractClass();
}

private function expectEvent($eventName, $event, $dispatcher)
{
$dispatcher->expects($this->once())->method('dispatch')->with($event, $eventName);
Expand Down
11 changes: 6 additions & 5 deletions Tests/Security/Guard/JWTTokenAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Lexik\Bundle\JWTAuthenticationBundle\Exception\InvalidTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\MissingTokenException;
use Lexik\Bundle\JWTAuthenticationBundle\Exception\UserNotFoundException;
use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationFailureResponse;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\PreAuthenticationJWTUserToken;
Expand All @@ -22,7 +21,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

Expand Down Expand Up @@ -166,11 +165,13 @@ public function testGetUserWithInvalidUserThrowsException()
$decodedToken->setPayload($payload);

$userProvider = $this->getUserProviderMock();
$e = new UserNotFoundException();
$e->setUserIdentifier('lexik');
$userProvider
->expects($this->once())
->method('loadUserByUsername')
->with($payload[$userIdClaim])
->will($this->throwException(new UsernameNotFoundException()));
->will($this->throwException($e));

try {
(new JWTTokenAuthenticator(
Expand All @@ -181,8 +182,8 @@ public function testGetUserWithInvalidUserThrowsException()
))->getUser($decodedToken, $userProvider);

$this->fail(sprintf('Expected exception of type "%s" to be thrown.', UserNotFoundException::class));
} catch (UsernameNotFoundException $e) {
$this->assertSame('lexik', $e->getUsername());
} catch (UserNotFoundException $e) {
$this->assertSame('lexik', $e->getUserIdentifier());
}
}

Expand Down
15 changes: 10 additions & 5 deletions Tests/Stubs/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,18 @@
*/
final class User implements UserInterface
{
private $username;
private $userIdentifier;
private $password;
private $roles;
private $email;

public function __construct($username, $password, $email = '', array $roles = [])
public function __construct($userIdentifier, $password, $email = '', array $roles = [])
{
if (empty($username)) {
if (empty($userIdentifier)) {
throw new \InvalidArgumentException('The username cannot be empty.');
}

$this->username = $username;
$this->userIdentifier = $userIdentifier;
$this->password = $password;
$this->roles = $roles;
$this->email = $email;
Expand Down Expand Up @@ -67,7 +67,12 @@ public function getSalt()
*/
public function getUsername()
{
return $this->username;
return $this->getUserIdentifier();
}

public function getUserIdentifier(): string
{
return $this->userIdentifier;
}

/**
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"symfony/security-bundle": "^4.4|^5.1"
},
"require-dev": {
"phpspec/prophecy": "^1.13",
"symfony/browser-kit": "^4.4|^5.1",
"symfony/console": "^4.4|^5.1",
"symfony/dom-crawler": "^4.4|^5.1",
Expand Down

0 comments on commit 656812a

Please sign in to comment.