From 08622392ac1df6dc5a11648a79e5b3bb7b0ee7da Mon Sep 17 00:00:00 2001 From: Spomky Date: Mon, 23 Jul 2018 23:17:03 +0200 Subject: [PATCH] Customizable User ID Claim --- CHANGELOG.md | 6 +++- DependencyInjection/Configuration.php | 4 +++ .../LexikJWTAuthenticationExtension.php | 3 ++ Encoder/LcobucciJWTEncoder.php | 2 +- Resources/config/deprecated.xml | 1 + Resources/config/jwt_manager.xml | 1 + .../Authentication/Provider/JWTProvider.php | 22 ++++++++++-- Security/Guard/JWTTokenAuthenticator.php | 13 +++---- Services/JWTManager.php | 21 +++++++++-- Services/JWTTokenManagerInterface.php | 7 ++++ .../CompleteTokenAuthenticationTest.php | 3 +- Tests/Functional/GetTokenTest.php | 4 +++ .../app/config/config_user_id_claim.yml | 8 +++++ .../Provider/JWTProviderTest.php | 12 +++---- .../Guard/JWTTokenAuthenticatorTest.php | 36 +++++++++++-------- Tests/Services/JWTManagerTest.php | 6 ++-- composer.json | 1 + phpunit.xml.dist | 3 -- 18 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 Tests/Functional/app/config/config_user_id_claim.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 58a30657..1d6c8fa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,11 @@ CHANGELOG ========= -For a diff between two versions https://github.com/lexik/LexikJWTAuthenticationBundle/compare/v1.0.0...v2.5.3 +For a diff between two versions https://github.com/lexik/LexikJWTAuthenticationBundle/compare/v1.0.0...v2.5.4 + +## [2.5.4](https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.5.4) (2018-08-2) + +* bug [\#542](https://github.com/lexik/LexikJWTAuthenticationBundle/pull/542) Fix missing implements breaking JWT header alteration ([tucksaun](https://github.com/tucksaun)) ## [2.5.3](https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.5.3) (2018-07-6) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 8c39b0ac..37a4b529 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -70,6 +70,10 @@ public function getConfigTreeBuilder() ->defaultValue('username') ->cannotBeEmpty() ->end() + ->scalarNode('user_id_claim') + ->defaultNull() + ->info('If null, the user ID clam will be have the same name as the one defined by the option "user_identity_field"') + ->end() ->append($this->getTokenExtractorsNode()) ->end(); diff --git a/DependencyInjection/LexikJWTAuthenticationExtension.php b/DependencyInjection/LexikJWTAuthenticationExtension.php index 7b9fb854..ba8dde6e 100644 --- a/DependencyInjection/LexikJWTAuthenticationExtension.php +++ b/DependencyInjection/LexikJWTAuthenticationExtension.php @@ -60,6 +60,9 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('lexik_jwt_authentication.token_ttl', $config['token_ttl']); $container->setParameter('lexik_jwt_authentication.clock_skew', $config['clock_skew']); $container->setParameter('lexik_jwt_authentication.user_identity_field', $config['user_identity_field']); + + $user_id_claim = $config['user_id_claim'] ? $config['user_id_claim'] : $config['user_identity_field']; + $container->setParameter('lexik_jwt_authentication.user_id_claim', $user_id_claim); $encoderConfig = $config['encoder']; if ('lexik_jwt_authentication.encoder.default' === $encoderConfig['service']) { diff --git a/Encoder/LcobucciJWTEncoder.php b/Encoder/LcobucciJWTEncoder.php index fbad6549..d2a5fe54 100644 --- a/Encoder/LcobucciJWTEncoder.php +++ b/Encoder/LcobucciJWTEncoder.php @@ -11,7 +11,7 @@ * * @author Robin Chalas */ -class LcobucciJWTEncoder implements JWTEncoderInterface +class LcobucciJWTEncoder implements JWTEncoderInterface, HeaderAwareJWTEncoderInterface { /** * @var JWSProviderInterface diff --git a/Resources/config/deprecated.xml b/Resources/config/deprecated.xml index fa58fa29..2597564d 100644 --- a/Resources/config/deprecated.xml +++ b/Resources/config/deprecated.xml @@ -9,6 +9,7 @@ + %lexik_jwt_authentication.user_id_claim% %lexik_jwt_authentication.user_identity_field% diff --git a/Resources/config/jwt_manager.xml b/Resources/config/jwt_manager.xml index 172984a0..a28b1c34 100644 --- a/Resources/config/jwt_manager.xml +++ b/Resources/config/jwt_manager.xml @@ -8,6 +8,7 @@ + %lexik_jwt_authentication.user_id_claim% %lexik_jwt_authentication.user_identity_field% diff --git a/Security/Authentication/Provider/JWTProvider.php b/Security/Authentication/Provider/JWTProvider.php index a77268d3..97693769 100644 --- a/Security/Authentication/Provider/JWTProvider.php +++ b/Security/Authentication/Provider/JWTProvider.php @@ -44,15 +44,22 @@ class JWTProvider implements AuthenticationProviderInterface */ protected $userIdentityField; + /** + * @var string + */ + private $userIdClaim; + /** * @param UserProviderInterface $userProvider * @param JWTManagerInterface $jwtManager * @param EventDispatcherInterface $dispatcher + * @param string $userIdClaim */ public function __construct( UserProviderInterface $userProvider, JWTManagerInterface $jwtManager, - EventDispatcherInterface $dispatcher + EventDispatcherInterface $dispatcher, + $userIdClaim ) { @trigger_error(sprintf('The "%s" class is deprecated since version 2.0 and will be removed in 3.0. See "%s" instead.', __CLASS__, JWTTokenAuthenticator::class), E_USER_DEPRECATED); @@ -60,6 +67,7 @@ public function __construct( $this->jwtManager = $jwtManager; $this->dispatcher = $dispatcher; $this->userIdentityField = 'username'; + $this->userIdClaim = $userIdClaim; } /** @@ -97,11 +105,11 @@ public function authenticate(TokenInterface $token) */ protected function getUserFromPayload(array $payload) { - if (!isset($payload[$this->userIdentityField])) { + if (!isset($payload[$this->userIdClaim])) { throw $this->createAuthenticationException(); } - return $this->userProvider->loadUserByUsername($payload[$this->userIdentityField]); + return $this->userProvider->loadUserByUsername($payload[$this->userIdClaim]); } /** @@ -128,6 +136,14 @@ public function setUserIdentityField($userIdentityField) $this->userIdentityField = $userIdentityField; } + /** + * @return string + */ + public function getUserIdClaim() + { + return $this->userIdClaim; + } + /** * @param JWTDecodeFailureException $previous * diff --git a/Security/Guard/JWTTokenAuthenticator.php b/Security/Guard/JWTTokenAuthenticator.php index 00be8907..43d87a38 100644 --- a/Security/Guard/JWTTokenAuthenticator.php +++ b/Security/Guard/JWTTokenAuthenticator.php @@ -141,19 +141,20 @@ public function getUser($preAuthToken, UserProviderInterface $userProvider) ); } - $payload = $preAuthToken->getPayload(); - $identityField = $this->jwtManager->getUserIdentityField(); + $payload = $preAuthToken->getPayload(); + $idClaim = $this->jwtManager->getUserIdClaim(); - if (!isset($payload[$identityField])) { - throw new InvalidPayloadException($identityField); + + if (!isset($payload[$idClaim])) { + throw new InvalidPayloadException($idClaim); } - $identity = $payload[$identityField]; + $identity = $payload[$idClaim]; try { $user = $this->loadUser($userProvider, $payload, $identity); } catch (UsernameNotFoundException $e) { - throw new UserNotFoundException($identityField, $identity); + throw new UserNotFoundException($idClaim, $identity); } $this->preAuthenticationTokenStorage->setToken($preAuthToken); diff --git a/Services/JWTManager.php b/Services/JWTManager.php index e84b2dea..c8d74780 100644 --- a/Services/JWTManager.php +++ b/Services/JWTManager.php @@ -36,15 +36,22 @@ class JWTManager implements JWTManagerInterface, JWTTokenManagerInterface */ protected $userIdentityField; + /** + * @var string + */ + protected $userIdClaim; + /** * @param JWTEncoderInterface $encoder * @param EventDispatcherInterface $dispatcher + * @param string $userIdClaim */ - public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher) + public function __construct(JWTEncoderInterface $encoder, EventDispatcherInterface $dispatcher, $userIdClaim) { $this->jwtEncoder = $encoder; $this->dispatcher = $dispatcher; $this->userIdentityField = 'username'; + $this->userIdClaim = $userIdClaim; } /** @@ -99,8 +106,8 @@ public function decode(TokenInterface $token) */ protected function addUserIdentityToPayload(UserInterface $user, array &$payload) { - $accessor = PropertyAccess::createPropertyAccessor(); - $payload[$this->userIdentityField] = $accessor->getValue($user, $this->userIdentityField); + $accessor = PropertyAccess::createPropertyAccessor(); + $payload[$this->userIdClaim] = $accessor->getValue($user, $this->userIdentityField); } /** @@ -118,4 +125,12 @@ public function setUserIdentityField($userIdentityField) { $this->userIdentityField = $userIdentityField; } + + /** + * @return string + */ + public function getUserIdClaim() + { + return $this->userIdClaim; + } } diff --git a/Services/JWTTokenManagerInterface.php b/Services/JWTTokenManagerInterface.php index 2d6c64cc..055969ad 100644 --- a/Services/JWTTokenManagerInterface.php +++ b/Services/JWTTokenManagerInterface.php @@ -40,4 +40,11 @@ public function setUserIdentityField($field); * @return string */ public function getUserIdentityField(); + + /** + * Returns the claim used as identifier to load an user from a JWT payload. + * + * @return string + */ + public function getUserIdClaim(); } diff --git a/Tests/Functional/CompleteTokenAuthenticationTest.php b/Tests/Functional/CompleteTokenAuthenticationTest.php index 9cd0b540..cc3a6c9a 100644 --- a/Tests/Functional/CompleteTokenAuthenticationTest.php +++ b/Tests/Functional/CompleteTokenAuthenticationTest.php @@ -88,6 +88,7 @@ public function testExpClaimIsNotSetIfNoTTL() { static::bootKernel(); $encoder = static::$kernel->getContainer()->get('lexik_jwt_authentication.encoder'); + $idClaim = static::$kernel->getContainer()->getParameter('lexik_jwt_authentication.user_id_claim'); $r = new \ReflectionProperty(get_class($encoder), 'jwsProvider'); $r->setAccessible(true); @@ -96,7 +97,7 @@ public function testExpClaimIsNotSetIfNoTTL() $this->ttl = null; }, $jwsProvider, get_class($jwsProvider))->__invoke(); - $token = $encoder->encode(['username' => 'lexik']); + $token = $encoder->encode([$idClaim => 'lexik']); $this->assertArrayNotHasKey('exp', $encoder->decode($token)); static::$client = static::createAuthenticatedClient($token); diff --git a/Tests/Functional/GetTokenTest.php b/Tests/Functional/GetTokenTest.php index b7f4f75e..27350460 100644 --- a/Tests/Functional/GetTokenTest.php +++ b/Tests/Functional/GetTokenTest.php @@ -2,6 +2,7 @@ namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional; +use Lcobucci\JWT\Parser; use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent; use Lexik\Bundle\JWTAuthenticationBundle\Events; use Lexik\Bundle\JWTAuthenticationBundle\Response\JWTAuthenticationSuccessResponse; @@ -41,6 +42,9 @@ public function testGetTokenWithCustomClaim() $this->assertArrayHasKey('custom', $payload, 'The payload should contains a "custom" claim.'); $this->assertSame('dummy', $payload['custom'], 'The "custom" claim should be equal to "dummy".'); + + $jws = (new Parser())->parse((string) $body['token']); + $this->assertArrayHasKey('foo', $jws->getHeaders(), 'The payload should contains a custom "foo" header.'); } public function testGetTokenFromInvalidCredentials() diff --git a/Tests/Functional/app/config/config_user_id_claim.yml b/Tests/Functional/app/config/config_user_id_claim.yml new file mode 100644 index 00000000..7a68fb77 --- /dev/null +++ b/Tests/Functional/app/config/config_user_id_claim.yml @@ -0,0 +1,8 @@ +imports: + - { resource: base_config.yml } + +lexik_jwt_authentication: + secret_key: '%kernel.root_dir%/../config/jwt/private.pem' + public_key: '%kernel.root_dir%/../config/jwt/public.pem' + pass_phrase: testing + user_id_claim: 'sub' diff --git a/Tests/Security/Authentication/Provider/JWTProviderTest.php b/Tests/Security/Authentication/Provider/JWTProviderTest.php index 87b1c19a..c34f2b40 100644 --- a/Tests/Security/Authentication/Provider/JWTProviderTest.php +++ b/Tests/Security/Authentication/Provider/JWTProviderTest.php @@ -21,7 +21,7 @@ class JWTProviderTest extends TestCase */ public function testSupports() { - $provider = new JWTProvider($this->getUserProviderMock(), $this->getJWTManagerMock(), $this->getEventDispatcherMock()); + $provider = new JWTProvider($this->getUserProviderMock(), $this->getJWTManagerMock(), $this->getEventDispatcherMock(), 'username'); /** @var TokenInterface $usernamePasswordToken */ $usernamePasswordToken = $this @@ -60,7 +60,7 @@ public function testAuthenticateWithInvalidJWT() $jwtManager = $this->getJWTManagerMock(); $jwtManager->expects($this->any())->method('decode')->will($this->returnValue(false)); - $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher); + $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher, 'username'); $provider->authenticate($jwtUserToken); } @@ -84,7 +84,7 @@ public function testAuthenticateWithoutUsername() $jwtManager = $this->getJWTManagerMock(); $jwtManager->expects($this->any())->method('decode')->will($this->returnValue(['foo' => 'bar'])); - $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher); + $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher, 'username'); $provider->authenticate($jwtUserToken); } @@ -109,7 +109,7 @@ public function testAuthenticateWithNotExistingUser() $jwtManager = $this->getJWTManagerMock(); $jwtManager->expects($this->any())->method('decode')->will($this->returnValue(['username' => 'user'])); - $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher); + $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher, 'username'); $provider->authenticate($jwtUserToken); } @@ -138,7 +138,7 @@ public function testAuthenticate() $jwtManager = $this->getJWTManagerMock(); $jwtManager->expects($this->any())->method('decode')->will($this->returnValue(['username' => 'user'])); - $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher); + $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher, 'username'); $this->assertInstanceOf( 'Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken', @@ -150,7 +150,7 @@ public function testAuthenticate() $jwtManager = $this->getJWTManagerMock(); $jwtManager->expects($this->any())->method('decode')->will($this->returnValue(['uid' => 'user'])); - $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher); + $provider = new JWTProvider($userProvider, $jwtManager, $eventDispatcher, 'uid'); $provider->setUserIdentityField('uid'); $this->assertInstanceOf( diff --git a/Tests/Security/Guard/JWTTokenAuthenticatorTest.php b/Tests/Security/Guard/JWTTokenAuthenticatorTest.php index f446b38f..5eebf6bd 100644 --- a/Tests/Security/Guard/JWTTokenAuthenticatorTest.php +++ b/Tests/Security/Guard/JWTTokenAuthenticatorTest.php @@ -94,10 +94,10 @@ public function testGetCredentialsReturnsNullWithoutToken() public function testGetUser() { - $userIdentityField = 'username'; - $payload = [$userIdentityField => 'lexik']; - $rawToken = 'token'; - $userRoles = ['ROLE_USER']; + $userIdClaim = 'sub'; + $payload = [$userIdClaim => 'lexik']; + $rawToken = 'token'; + $userRoles = ['ROLE_USER']; $userStub = new AdvancedUserStub('lexik', 'password', 'user@gmail.com', $userRoles); @@ -108,11 +108,11 @@ public function testGetUser() $userProvider ->expects($this->once()) ->method('loadUserByUsername') - ->with($payload[$userIdentityField]) + ->with($payload[$userIdClaim]) ->willReturn($userStub); $authenticator = new JWTTokenAuthenticator( - $this->getJWTManagerMock('username'), + $this->getJWTManagerMock(null, $userIdClaim), $this->getEventDispatcherMock(), $this->getTokenExtractorMock() ); @@ -127,7 +127,7 @@ public function testGetUserWithInvalidPayloadThrowsException() try { (new JWTTokenAuthenticator( - $this->getJWTManagerMock('username'), + $this->getJWTManagerMock(null, 'username'), $this->getEventDispatcherMock(), $this->getTokenExtractorMock() ))->getUser($decodedToken, $this->getUserProviderMock()); @@ -153,8 +153,8 @@ public function testGetUserWithInvalidFirstArg() public function testGetUserWithInvalidUserThrowsException() { - $userIdentityField = 'username'; - $payload = [$userIdentityField => 'lexik']; + $userIdClaim = 'username'; + $payload = [$userIdClaim => 'lexik']; $decodedToken = new PreAuthenticationJWTUserToken('rawToken'); $decodedToken->setPayload($payload); @@ -163,12 +163,12 @@ public function testGetUserWithInvalidUserThrowsException() $userProvider ->expects($this->once()) ->method('loadUserByUsername') - ->with($payload[$userIdentityField]) + ->with($payload[$userIdClaim]) ->will($this->throwException(new UsernameNotFoundException())); try { (new JWTTokenAuthenticator( - $this->getJWTManagerMock('username'), + $this->getJWTManagerMock(null, 'username'), $this->getEventDispatcherMock(), $this->getTokenExtractorMock() ))->getUser($decodedToken, $userProvider); @@ -183,7 +183,7 @@ public function testCreateAuthenticatedToken() { $rawToken = 'token'; $userRoles = ['ROLE_USER']; - $payload = ['username' => 'lexik']; + $payload = ['sub' => 'lexik']; $userStub = new AdvancedUserStub('lexik', 'password', 'user@gmail.com', $userRoles); $decodedToken = new PreAuthenticationJWTUserToken($rawToken); @@ -198,7 +198,7 @@ public function testCreateAuthenticatedToken() ->with(Events::JWT_AUTHENTICATED, new JWTAuthenticatedEvent($payload, $jwtUserToken)); $authenticator = new JWTTokenAuthenticator( - $this->getJWTManagerMock('username'), + $this->getJWTManagerMock(null, 'sub'), $dispatcher, $this->getTokenExtractorMock() ); @@ -207,7 +207,7 @@ public function testCreateAuthenticatedToken() $userProvider ->expects($this->once()) ->method('loadUserByUsername') - ->with($payload['username']) + ->with($payload['sub']) ->willReturn($userStub); $authenticator->getUser($decodedToken, $userProvider); @@ -306,7 +306,7 @@ public function testSupportsRememberMe() ); } - private function getJWTManagerMock($userIdentityField = null) + private function getJWTManagerMock($userIdentityField = null, $userIdClaim = null) { $jwtManager = $this->getMockBuilder(JWTTokenManagerInterface::class) ->disableOriginalConstructor() @@ -318,6 +318,12 @@ private function getJWTManagerMock($userIdentityField = null) ->method('getUserIdentityField') ->willReturn($userIdentityField); } + if (null !== $userIdClaim) { + $jwtManager + ->expects($this->once()) + ->method('getUserIdClaim') + ->willReturn($userIdClaim); + } return $jwtManager; } diff --git a/Tests/Services/JWTManagerTest.php b/Tests/Services/JWTManagerTest.php index 49045ff2..e8599ef2 100644 --- a/Tests/Services/JWTManagerTest.php +++ b/Tests/Services/JWTManagerTest.php @@ -44,7 +44,7 @@ public function testCreate() ->method('encode') ->willReturn('secrettoken'); - $manager = new JWTManager($encoder, $dispatcher); + $manager = new JWTManager($encoder, $dispatcher, 'username'); $this->assertEquals('secrettoken', $manager->create(new User('user', 'password'))); } @@ -68,7 +68,7 @@ public function testDecode() ->method('decode') ->willReturn(['foo' => 'bar']); - $manager = new JWTManager($encoder, $dispatcher); + $manager = new JWTManager($encoder, $dispatcher, 'username'); $this->assertEquals(['foo' => 'bar'], $manager->decode($this->getJWTUserTokenMock())); } @@ -100,7 +100,7 @@ public function testIdentityField() ->method('encode') ->willReturn('secrettoken'); - $manager = new JWTManager($encoder, $dispatcher); + $manager = new JWTManager($encoder, $dispatcher, 'username'); $manager->setUserIdentityField('email'); $this->assertEquals('secrettoken', $manager->create(new CustomUser('user', 'password', 'victuxbb@gmail.com'))); } diff --git a/composer.json b/composer.json index fea559d3..d74169ae 100644 --- a/composer.json +++ b/composer.json @@ -70,6 +70,7 @@ "vendor/bin/simple-phpunit", "ENCODER=lcobucci vendor/bin/simple-phpunit", "ENCODER=lcobucci ALGORITHM=HS256 vendor/bin/simple-phpunit", + "ENCODER=user_id_claim vendor/bin/simple-phpunit", "PROVIDER=lexik_jwt vendor/bin/simple-phpunit" ] } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 527ebc1a..e0b14d35 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,13 +2,11 @@ @@ -28,5 +26,4 @@ -