diff --git a/README.md b/README.md index 3747217..c3a1e85 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ For an example see [swagger-bundle-example](https://github.com/kleijnweb/swagger *NOTE:* Looking for PHP <7.0 and Symfony <2.8.7 support? Use a 0.x version. -## Install And Configure +## Install Install using composer (`composer require kleijnweb/jwt-bundle`). You want to check out the [release page](https://github.com/kleijnweb/jwt-bundle/releases) to ensure you are getting what you want and optionally verify your download. @@ -125,7 +125,7 @@ class SimpleSecretLoader implements SecretLoader } ``` -You could use any information available in the token, such as the `kid`, `alg` or any custom claims. You cannot configure both `secret` and `loader`. Be sure to throw an `AuthenticationException` when appropriate (eg missing claims needed for loading secret). +You could use any information available in the token, such as the `kid`, `alg` or any custom claims. You cannot configure both `secret` and `loader`. Be sure to throw an `AuthenticationException` when appropriate (eg missing claims needed for loading secret). ### Integration Into Symfony Security @@ -147,22 +147,6 @@ security: Using the bundled user provider is optional. This will produce user objects from the token data alone with roles produced from the `aud` claim (and `IS_AUTHENTICATED_FULLY` whether `aud` was set or not). -For BC reasons, the following also works: - -```yml -security: - firewalls: - default: - stateless: true - simple_preauth: - authenticator: jwt.authenticator - provider: jwt - - providers: - jwt: - id: jwt.user_provider -``` - ### Assigning audience to user roles using an alternate UserProvider JwtBundle can assign the audience claims in the JwtToken to the User objects user roles properties. Ideally, this is done in the UserProvider, so that the groups cannot be modified. @@ -172,6 +156,24 @@ This behavior may be removed in future versions. _NOTE:_ This function *only* copies the the roles from the token. +### Issuing Token + +Issuing tokens is currently limited to `HS256`. To create a token string: + +```php +$token = new JwtToken([ + 'header' => [ + 'alg' => 'HS256', + 'typ' => 'JWT', + 'kid' => 'Optional Key ID' + ], + 'claims' => [ /* Array of claims */ ], + 'secret' => 'Your Secret' +]); + +$token->getTokenString(); +``` + ## License KleijnWeb\JwtBundle is made available under the terms of the [LGPL, version 3.0](https://spdx.org/licenses/LGPL-3.0.html#licenseText). diff --git a/src/DependencyInjection/KleijnWebJwtExtension.php b/src/DependencyInjection/KleijnWebJwtExtension.php index b0f141e..dd6e612 100644 --- a/src/DependencyInjection/KleijnWebJwtExtension.php +++ b/src/DependencyInjection/KleijnWebJwtExtension.php @@ -44,8 +44,8 @@ public function load(array $configs, ContainerBuilder $container) $keys[] = $keyDefinition; } - $container->getDefinition('jwt.authenticator')->addArgument($keys); $container->getDefinition('jwt.security.authentication.provider')->addArgument($keys); + $container->getDefinition('jwt.token_issuer')->addArgument($keys); } diff --git a/src/Firewall/JwtAuthenticationListener.php b/src/Firewall/JwtAuthenticationListener.php index 54c128a..ceaee6d 100644 --- a/src/Firewall/JwtAuthenticationListener.php +++ b/src/Firewall/JwtAuthenticationListener.php @@ -66,7 +66,7 @@ public function handle(GetResponseEvent $event) * * @return JwtAuthenticationToken|null */ - public function createToken(Request $request) + private function createToken(Request $request) { $tokenString = $request->headers->get($this->header); diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml index d6c57a5..920be7b 100644 --- a/src/Resources/config/services.yml +++ b/src/Resources/config/services.yml @@ -1,12 +1,12 @@ services: - jwt.authenticator: - class: KleijnWeb\JwtBundle\Authenticator\Authenticator - public: false - jwt.user_provider: class: KleijnWeb\JwtBundle\User\JwtUserProvider public: false + jwt.token_issuer: + class: KleijnWeb\JwtBundle\Jwt\TokenIssuer + public: true + jwt.security.authentication.provider: class: KleijnWeb\JwtBundle\Authentication\JwtAuthenticationProvider arguments: diff --git a/tests/functional/FunctionalTest.php b/tests/functional/FunctionalTest.php index 95a0d33..a1a791a 100644 --- a/tests/functional/FunctionalTest.php +++ b/tests/functional/FunctionalTest.php @@ -8,7 +8,7 @@ namespace KleijnWeb\JwtBundle\Tests\Functional; -use KleijnWeb\JwtBundle\Tests\Jwt\JwtAuthenticationProviderTest; +use KleijnWeb\JwtBundle\Tests\Authentication\JwtAuthenticationProviderTest; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException; diff --git a/tests/unit/Firewall/JwtAuthenticationProviderTest.php b/tests/unit/Authentication/JwtAuthenticationProviderTest.php similarity index 92% rename from tests/unit/Firewall/JwtAuthenticationProviderTest.php rename to tests/unit/Authentication/JwtAuthenticationProviderTest.php index 8e0054b..e737b69 100644 --- a/tests/unit/Firewall/JwtAuthenticationProviderTest.php +++ b/tests/unit/Authentication/JwtAuthenticationProviderTest.php @@ -5,8 +5,9 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace KleijnWeb\JwtBundle\Tests\Jwt; +namespace KleijnWeb\JwtBundle\Tests\Authentication; +use KleijnWeb\JwtBundle\Authentication\JwtAuthenticatedToken; use KleijnWeb\JwtBundle\Authentication\JwtAuthenticationProvider; use KleijnWeb\JwtBundle\Authentication\JwtAuthenticationToken; use KleijnWeb\JwtBundle\Jwt\JwtKey; @@ -151,6 +152,24 @@ public function authenticateTokenWillThrowExceptionWhenTokenUnsupportedType() $jwtAuthenticationProvider->authenticate($anonToken); } + /** + * @test + */ + public function authenticateWillReturnAuthenticatedToken() + { + $jwtAuthenticationProvider = new JwtAuthenticationProvider($this->standardUserProviderMock, $this->keys); + $authToken = new JwtAuthenticationToken([], self::TEST_TOKEN); + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->standardUserProviderMock; + $mock + ->expects($this->once()) + ->method('loadUserByUsername') + ->willReturn(new User('john', 'hi there')); + + $this->assertInstanceOf(JwtAuthenticatedToken::class, $jwtAuthenticationProvider->authenticate($authToken)); + } + /** * @test */ diff --git a/tests/unit/Classes/User.php b/tests/unit/Classes/User.php deleted file mode 100644 index 84c8b91..0000000 --- a/tests/unit/Classes/User.php +++ /dev/null @@ -1,120 +0,0 @@ - - */ -final class User implements AdvancedUserInterface -{ - private $username; - private $password; - private $enabled; - private $accountNonExpired; - private $credentialsNonExpired; - private $accountNonLocked; - private $roles; - - public function __construct($username, $password, array $roles = array(), $enabled = true, $userNonExpired = true, $credentialsNonExpired = true, $userNonLocked = true) - { - if ('' === $username || null === $username) { - throw new \InvalidArgumentException('The username cannot be empty.'); - } - - $this->username = $username; - $this->password = $password; - $this->enabled = $enabled; - $this->accountNonExpired = $userNonExpired; - $this->credentialsNonExpired = $credentialsNonExpired; - $this->accountNonLocked = $userNonLocked; - $this->roles = $roles; - } - - public function __toString() - { - return $this->getUsername(); - } - - /** - * {@inheritdoc} - */ - public function getRoles() - { - return $this->roles; - } - - /** - * @param $role - */ - public function addRole($role) - { - $this->roles[] = $role; - } - - /** - * {@inheritdoc} - */ - public function getPassword() - { - return $this->password; - } - - /** - * {@inheritdoc} - */ - public function getSalt() - { - } - - /** - * {@inheritdoc} - */ - public function getUsername() - { - return $this->username; - } - - /** - * {@inheritdoc} - */ - public function isAccountNonExpired() - { - return $this->accountNonExpired; - } - - /** - * {@inheritdoc} - */ - public function isAccountNonLocked() - { - return $this->accountNonLocked; - } - - /** - * {@inheritdoc} - */ - public function isCredentialsNonExpired() - { - return $this->credentialsNonExpired; - } - - /** - * {@inheritdoc} - */ - public function isEnabled() - { - return $this->enabled; - } - - /** - * {@inheritdoc} - */ - public function eraseCredentials() - { - } -} diff --git a/tests/unit/Firewall/JwtAuthenticationListenerTest.php b/tests/unit/Firewall/JwtAuthenticationListenerTest.php new file mode 100644 index 0000000..753e0f8 --- /dev/null +++ b/tests/unit/Firewall/JwtAuthenticationListenerTest.php @@ -0,0 +1,181 @@ + + */ +class JwtAuthenticationListenerTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var AuthenticationManagerInterface + */ + private $authenticationManagerMock; + + /** + * @var TokenStorageInterface + */ + private $tokenStorageMock; + + protected function setUp() + { + $this->tokenStorageMock = $this->getMockForAbstractClass(TokenStorageInterface::class); + $this->authenticationManagerMock = $this->getMockForAbstractClass(AuthenticationManagerInterface::class); + } + + /** + * @test + */ + public function willSkipAuthenticationIfHeaderNotSetInRequest() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->never())->method('authenticate'); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock)) + ->handle($this->createKernelEventWithRequest(new Request())); + } + + /** + * @test + */ + public function canCreateTokenFromBearerHeaderByDefault() + { + $tokenString = 'TheJwtToken'; + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->once()) + ->method('authenticate') + ->with($this->callback(function (JwtAuthenticationToken $token) use ($tokenString) { + return $token->getCredentials() === $tokenString; + })); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock)) + ->handle($this->createKernelEventWithRequest( + $this->createRequestWithServerVar('HTTP_AUTHORIZATION', "Bearer $tokenString")) + ); + + } + + /** + * @test + */ + public function canCreateTokenFromAuthHeaderWithoutBearerPrefixByDefault() + { + $tokenString = 'TheJwtToken'; + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->once()) + ->method('authenticate') + ->with($this->callback(function (JwtAuthenticationToken $token) use ($tokenString) { + return $token->getCredentials() === $tokenString; + })); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock)) + ->handle($this->createKernelEventWithRequest( + $this->createRequestWithServerVar('HTTP_AUTHORIZATION', $tokenString)) + ); + } + + /** + * @test + */ + public function canCreateTokenFromCustomHeader() + { + $tokenString = 'TheJwtToken'; + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->once()) + ->method('authenticate') + ->with($this->callback(function (JwtAuthenticationToken $token) use ($tokenString) { + return $token->getCredentials() === $tokenString; + })); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock, 'X-Token')) + ->handle($this->createKernelEventWithRequest( + $this->createRequestWithServerVar('HTTP_X_TOKEN', $tokenString)) + ); + } + + /** + * @test + */ + public function willSkipAuthenticationIfCustomHeaderNotSetInRequest() + { + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->never())->method('authenticate'); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock, 'X-Token')) + ->handle($this->createKernelEventWithRequest( + $this->createRequestWithServerVar('HTTP_AUTHORIZATION', 'something')) + ); + } + + /** + * @test + */ + public function willSetTokenReturnedByAuthenticationHeaderOnStorage() + { + + $userStub = $this->getMockForAbstractClass(UserInterface::class); + $userStub->expects($this->any())->method('getRoles')->willReturn([]); + + $authenticatedToken = new JwtAuthenticatedToken($userStub); + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->authenticationManagerMock; + $mock->expects($this->once()) + ->method('authenticate') + ->willReturnCallback(function () use($authenticatedToken) { + return $authenticatedToken; + }); + + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + $mock = $this->tokenStorageMock; + $mock->expects($this->once()) + ->method('setToken') + ->with($authenticatedToken); + + (new JwtAuthenticationListener($this->tokenStorageMock, $this->authenticationManagerMock)) + ->handle($this->createKernelEventWithRequest( + $this->createRequestWithServerVar('HTTP_AUTHORIZATION', 'something')) + ); + } + + private function createRequestWithServerVar(string $name, string $value): Request + { + return new Request([], [], [], [], [], [$name => $value]); + } + + private function createKernelEventWithRequest(Request $request): GetResponseEvent + { + $mock = $this->getMockBuilder(GetResponseEvent::class) + ->disableOriginalConstructor() + ->getMock(); + $mock + ->expects($this->any()) + ->method('getRequest') + ->willReturn($request); + + return $mock; + } + +}