diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c224c32 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,61 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "Build" + +on: + pull_request: + branches: + - "v*.*" + push: + branches: + - "v*.*" + +jobs: + tests: + name: "Tests" + + runs-on: "ubuntu-latest" + + strategy: + matrix: + php-version: + - "8.1" + + steps: + - name: "Checkout" + uses: "actions/checkout@v2" + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + coverage: "none" + php-version: "${{ matrix.php-version }}" + extensions: mbstring + + - name: "Cache dependencies" + uses: "actions/cache@v2" + with: + path: "~/.composer/cache" + key: "php-${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }}" + restore-keys: "php-${{ matrix.php-version }}-composer-" + + - name: "Install dependencies" + run: "composer update --prefer-dist --no-interaction --no-progress --no-suggest" + + - name: "Security Checker" + run: | + wget -O local-php-security-checker https://github.com/fabpot/local-php-security-checker/releases/latest/download/local-php-security-checker_1.2.0_linux_amd64 + chmod +x local-php-security-checker + ./local-php-security-checker + + - name: "Code Style" + run: | + wget -O php-cs-fixer https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/latest/download/php-cs-fixer.phar + chmod a+x php-cs-fixer + ./php-cs-fixer fix --dry-run --stop-on-violation --using-cache=no + + - name: "Static Analysis" + run: "vendor/bin/phpstan" + + - name: "Tests" + run: "vendor/bin/behat" diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 97% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 917b4d1..e9c8b7d 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -16,7 +16,7 @@ ->in(__DIR__.'/Tests') ; -return PhpCsFixer\Config::create() +return (new PhpCsFixer\Config()) ->setRules([ '@PSR1' => true, '@PSR2' => true, @@ -55,7 +55,7 @@ 'comment_type' => 'all_multiline', ], 'php_unit_test_annotation' => [ - 'case' => 'snake', + //'case' => 'snake', 'style' => 'annotation', ], 'php_unit_test_case_static_method_calls' => true, diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index b4002f1..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,36 +0,0 @@ -before_commands: - - "composer install --prefer-dist" - -checks: - php: - code_rating: true - duplication: false - -tools: - php_sim: false - php_changetracking: true - sensiolabs_security_checker: true - php_mess_detector: true - php_code_sniffer: true - php_analyzer: true - php_code_coverage: false - php_cpd: true - php_pdepend: - excluded_dirs: [vendor/*, Tests/*] -filter: - excluded_paths: [vendor/*, Tests/*] -build_failure_conditions: - - 'elements.rating(<= C).exists' # No classes/methods with a rating of C or worse - - 'elements.rating(<= C).new.exists' # No new classes/methods with a rating of C or worse - - 'patches.label("Doc Comments").exists' # No doc comments patches allowed - - 'patches.label("Spacing").new.count > 1' # More than 1 new spacing patch - - 'issues.label("coding-style").exists' # No coding style issues allowed - - 'issues.label("coding-style").new.exists' # No new coding style issues allowed - - 'issues.severity(>= MAJOR).new.exists' # New issues of major or higher severity - - 'project.metric("scrutinizer.quality", < 9)' # Code Quality Rating drops below 9 -build: - nodes: - analysis: - tests: - override: - - php-scrutinizer-run \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 01b2bef..0000000 --- a/.travis.yml +++ /dev/null @@ -1,48 +0,0 @@ -language: php -sudo: false - -cache: - directories: - - $HOME/.composer/cache - - vendor - -php: - - 7.2 - - 7.3 - - 7.4 - - nightly - -before_install: - - mv ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini{,.disabled} || echo "xdebug not available" - - echo "memory_limit=2G" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/travis.ini - -install: travis_retry composer install - -script: - - vendor/bin/behat - -jobs: - allow_failures: - - php: nightly - - include: - - stage: Metrics and quality - env: STATIC_ANALYSIS - script: - - ./vendor/bin/phpstan analyse - - - stage: Metrics and quality - env: CODING_STANDARDS - before_script: - - wget https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer - - chmod a+x php-cs-fixer - script: - - ./php-cs-fixer fix --dry-run --stop-on-violation --using-cache=no - - - stage: Security Check - env: SECURITY_CHECK - before_script: - - wget -c https://get.sensiolabs.org/security-checker.phar - - chmod +x security-checker.phar - script: - - ./security-checker.phar security:check diff --git a/Checker/AlgHeaderChecker.php b/Checker/AlgHeaderChecker.php index ab734bb..99da217 100644 --- a/Checker/AlgHeaderChecker.php +++ b/Checker/AlgHeaderChecker.php @@ -16,7 +16,6 @@ use function is_string; use Jose\Component\Checker\HeaderChecker; use Jose\Component\Checker\InvalidHeaderException; -use function Safe\sprintf; final class AlgHeaderChecker implements HeaderChecker { diff --git a/Checker/EncHeaderChecker.php b/Checker/EncHeaderChecker.php index f4c87e4..25587e0 100644 --- a/Checker/EncHeaderChecker.php +++ b/Checker/EncHeaderChecker.php @@ -16,7 +16,6 @@ use function is_string; use Jose\Component\Checker\HeaderChecker; use Jose\Component\Checker\InvalidHeaderException; -use function Safe\sprintf; final class EncHeaderChecker implements HeaderChecker { diff --git a/Checker/IssuerChecker.php b/Checker/IssuerChecker.php index 728bd7a..d410294 100644 --- a/Checker/IssuerChecker.php +++ b/Checker/IssuerChecker.php @@ -16,7 +16,6 @@ use Exception; use Jose\Component\Checker\ClaimChecker; use Jose\Component\Checker\HeaderChecker; -use function Safe\sprintf; final class IssuerChecker implements ClaimChecker, HeaderChecker { diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 36ddb89..01f20ec 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -30,6 +30,8 @@ public function getConfigTreeBuilder(): TreeBuilder if (!$rootNode instanceof ArrayNodeDefinition) { throw new RuntimeException('Invalid root node'); } + + // @phpstan-ignore-next-line $rootNode ->validate() ->ifTrue(static function (array $config): bool { @@ -97,6 +99,7 @@ public function getConfigTreeBuilder(): TreeBuilder private function addEncryptionSection(ArrayNodeDefinition $node): void { + // @phpstan-ignore-next-line $node ->addDefaultsIfNotSet() ->children() diff --git a/DependencyInjection/SpomkyLabsLexikJoseExtension.php b/DependencyInjection/SpomkyLabsLexikJoseExtension.php index 10b2ab7..2153ddc 100644 --- a/DependencyInjection/SpomkyLabsLexikJoseExtension.php +++ b/DependencyInjection/SpomkyLabsLexikJoseExtension.php @@ -27,7 +27,7 @@ final class SpomkyLabsLexikJoseExtension extends Extension implements PrependExt /** * {@inheritdoc} */ - public function getAlias() + public function getAlias(): string { return 'lexik_jose'; } @@ -74,8 +74,8 @@ public function loadEncryptionServices(ContainerBuilder $container): void public function prepend(ContainerBuilder $container): void { - $isDebug = $container->getParameter('kernel.debug'); - $bridgeConfig = current($container->getExtensionConfig($this->getAlias())); + $isDebug = (bool) $container->getParameter('kernel.debug'); + $bridgeConfig = (array) current($container->getExtensionConfig($this->getAlias())); if (!array_key_exists('claim_checked', $bridgeConfig)) { $bridgeConfig['claim_checked'] = []; } diff --git a/Encoder/LexikJoseEncoder.php b/Encoder/LexikJoseEncoder.php index b4a1fa6..15ad1c5 100644 --- a/Encoder/LexikJoseEncoder.php +++ b/Encoder/LexikJoseEncoder.php @@ -13,7 +13,6 @@ namespace SpomkyLabs\LexikJoseBundle\Encoder; -use Base64Url\Base64Url; use Exception; use function is_string; use Jose\Component\Checker\ClaimCheckerManager; @@ -33,7 +32,7 @@ use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTDecodeFailureException; use Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException; -use function Safe\sprintf; +use ParagonIE\ConstantTime\Base64UrlSafe; /** * Json Web Token encoder/decoder. @@ -234,6 +233,7 @@ public function decode($token): array $reason = JWTDecodeFailureException::EXPIRED_TOKEN; break; + default: $reason = JWTDecodeFailureException::INVALID_TOKEN; } @@ -311,7 +311,7 @@ private function verify(string $token): array private function getAdditionalPayload(): array { return [ - 'jti' => Base64Url::encode(random_bytes(64)), + 'jti' => Base64UrlSafe::encode(random_bytes(64)), 'exp' => time() + $this->ttl, 'iat' => time(), 'iss' => $this->issuer, diff --git a/Resources/config/services.php b/Resources/config/services.php index bb574a3..0cd32ff 100644 --- a/Resources/config/services.php +++ b/Resources/config/services.php @@ -16,7 +16,7 @@ use SpomkyLabs\LexikJoseBundle\Checker\AlgHeaderChecker; use SpomkyLabs\LexikJoseBundle\Encoder\LexikJoseEncoder; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; -use function Symfony\Component\DependencyInjection\Loader\Configurator\ref; +use function Symfony\Component\DependencyInjection\Loader\Configurator\service; return static function (ContainerConfigurator $container): void { $container = $container->services()->defaults() @@ -27,11 +27,11 @@ $container->set(LexikJoseEncoder::class) ->args([ - ref('jose.jws_builder.lexik_jose'), - ref('jose.jws_verifier.lexik_jose'), - ref('jose.claim_checker.lexik_jose'), - ref('jose.header_checker.lexik_jose_signature'), - ref('jose.key_set.lexik_jose_bridge.signature'), + service('jose.jws_builder.lexik_jose'), + service('jose.jws_verifier.lexik_jose'), + service('jose.claim_checker.lexik_jose'), + service('jose.header_checker.lexik_jose_signature'), + service('jose.key_set.lexik_jose_bridge.signature'), '%lexik_jose_bridge.encoder.key_index%', '%lexik_jose_bridge.encoder.signature_algorithm%', '%lexik_jose_bridge.encoder.issuer%', diff --git a/Tests/Bundle/TestBundle/Controller/ApiController.php b/Tests/Bundle/TestBundle/Controller/ApiController.php index d4dbbe5..62231d3 100644 --- a/Tests/Bundle/TestBundle/Controller/ApiController.php +++ b/Tests/Bundle/TestBundle/Controller/ApiController.php @@ -17,6 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; /** * @Route("/api") @@ -26,13 +27,13 @@ final class ApiController extends AbstractController /** * @Route("/anonymous") */ - public function anonymousAction(): Response + public function anonymousAction(TokenStorageInterface $tokenStorage): Response { - $user = $this->getUser(); + $user = $tokenStorage->getToken()?->getUser(); if (null === $user) { $message = 'Hello anonymous!'; } else { - $message = "Hello {$user->getUsername()}!"; + $message = "Hello {$user->getUserIdentifier()}!"; } return new Response($message); @@ -42,10 +43,10 @@ public function anonymousAction(): Response * @Route("/hello") * @IsGranted("ROLE_USER") */ - public function helloAction(): Response + public function helloAction(TokenStorageInterface $tokenStorage): Response { - $user = $this->getUser(); - $message = "Hello {$user->getUsername()}!"; + $user = $tokenStorage->getToken()?->getUser(); + $message = "Hello {$user->getUserIdentifier()}!"; return new Response($message); } @@ -54,10 +55,10 @@ public function helloAction(): Response * @Route("/admin") * @IsGranted("ROLE_ADMIN") */ - public function adminAction(): Response + public function adminAction(TokenStorageInterface $tokenStorage): Response { - $user = $this->getUser(); - $message = "Hello {$user->getUsername()}!"; + $user = $tokenStorage->getToken()?->getUser(); + $message = "Hello {$user->getUserIdentifier()}!"; return new Response($message); } diff --git a/Tests/Bundle/TestBundle/Controller/LoginController.php b/Tests/Bundle/TestBundle/Controller/LoginController.php index 83e19ff..63693a1 100644 --- a/Tests/Bundle/TestBundle/Controller/LoginController.php +++ b/Tests/Bundle/TestBundle/Controller/LoginController.php @@ -16,6 +16,7 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; final class LoginController extends AbstractController { @@ -23,9 +24,8 @@ final class LoginController extends AbstractController * @Route("/login", name="login") * @Template */ - public function loginAction(): array + public function loginAction(AuthenticationUtils $authenticationUtils): array { - $authenticationUtils = $this->get('security.authentication_utils'); $error = $authenticationUtils->getLastAuthenticationError(); $lastUsername = $authenticationUtils->getLastUsername(); diff --git a/Tests/Context/FeatureContext.php b/Tests/Context/FeatureContext.php index 7c94001..57b5599 100644 --- a/Tests/Context/FeatureContext.php +++ b/Tests/Context/FeatureContext.php @@ -15,15 +15,88 @@ use Behat\Behat\Context\SnippetAcceptingContext; use Behat\MinkExtension\Context\MinkContext; -use Behat\Symfony2Extension\Context\KernelDictionary; +use Jose\Bundle\JoseFramework\Services\JWEBuilder; +use Jose\Bundle\JoseFramework\Services\JWSBuilder; +use Jose\Component\Core\JWKSet; +use SpomkyLabs\LexikJoseBundle\Encoder\LexikJoseEncoder; +use SpomkyLabs\TestBundle\EventListener\JWTListener; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Behat context class. */ final class FeatureContext extends MinkContext implements SnippetAcceptingContext { - use KernelDictionary; use ResponseContext; use LoginContext; use RequestContext; + + public function __construct(private ContainerInterface $container) + { + } + + public function getJWTListener(): JWTListener + { + return $this->container->get('acme_api.event.jwt_created_listener'); + } + + public function getJWSBuilder(): JWSBuilder + { + return $this->container->get('jose.jws_builder.lexik_jose'); + } + + public function getJWEBuilder(): JWEBuilder + { + return $this->container->get('jose.jwe_builder.lexik_jose'); + } + + public function getEncoder(): LexikJoseEncoder + { + return $this->container->get('lexik_jwt_authentication.encoder'); + } + + public function getJWKSetSignature(): JWKSet + { + return $this->container->get('jose.key_set.lexik_jose_bridge.signature'); + } + + public function getJWKSetEncryption(): JWKSet + { + return $this->container->get('jose.key_set.lexik_jose_bridge.encryption'); + } + + public function getIssuer(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.issuer'); + } + + public function getAudience(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.audience'); + } + + public function getSignatureAlgorithm(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.signature_algorithm'); + } + + public function getKeyEncryptionAlgorithm(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.encryption.key_encryption_algorithm'); + } + + public function getContentEncryptionAlgorithm(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.encryption.content_encryption_algorithm'); + } + + public function getEncoderKeyIndex(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.key_index'); + } + + public function getEncoderEncryptionKeyIndex(): string + { + return $this->container->getParameter('lexik_jose_bridge.encoder.encryption.key_index'); + } } diff --git a/Tests/Context/LoginContext.php b/Tests/Context/LoginContext.php index f08a3cb..6426a1e 100644 --- a/Tests/Context/LoginContext.php +++ b/Tests/Context/LoginContext.php @@ -16,13 +16,14 @@ use function array_key_exists; use Exception; use Jose\Component\Core\JWK; +use Jose\Component\Core\JWKSet; use Jose\Component\Core\Util\JsonConverter; use Jose\Component\Encryption\JWEBuilder; use Jose\Component\Encryption\Serializer\CompactSerializer as JWECompactSerializer; use Jose\Component\Signature\JWSBuilder; use Jose\Component\Signature\Serializer\CompactSerializer as JWSCompactSerializer; use Lexik\Bundle\JWTAuthenticationBundle\Encoder\JWTEncoderInterface; -use Symfony\Component\DependencyInjection\ContainerInterface; +use SpomkyLabs\LexikJoseBundle\Encoder\LexikJoseEncoder; /** * Behat context class. @@ -41,10 +42,29 @@ trait LoginContext */ abstract public function getSession($name = null); - /** - * @return ContainerInterface - */ - abstract public function getContainer(); + abstract public function getJWSBuilder(): JWSBuilder; + + abstract public function getJWEBuilder(): JWEBuilder; + + abstract public function getEncoder(): LexikJoseEncoder; + + abstract public function getJWKSetSignature(): JWKSet; + + abstract public function getJWKSetEncryption(): JWKSet; + + abstract public function getIssuer(): string; + + abstract public function getAudience(): string; + + abstract public function getSignatureAlgorithm(): string; + + abstract public function getKeyEncryptionAlgorithm(): string; + + abstract public function getContentEncryptionAlgorithm(): string; + + abstract public function getEncoderKeyIndex(): string; + + abstract public function getEncoderEncryptionKeyIndex(): string; /** * @return null|string @@ -60,7 +80,7 @@ public function getToken() public function iHaveAValidSignedToken() { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = $this->getBasicPayload(); $signatureKey = $this->getSignatureKey(); $jwt = $jwsBuilder @@ -82,8 +102,9 @@ public function iHaveAValidSignedToken() public function theTokenMustContainTheClaimWithValue($claim, $value) { $this->theTokenMustContainTheClaim($claim); + /** @var JWTEncoderInterface $encoder */ - $encoder = $this->getContainer()->get('lexik_jwt_authentication.encoder'); + $encoder = $this->getEncoder(); $token_decoded = $encoder->decode($this->getToken()); if ($value !== $token_decoded[$claim]) { @@ -99,7 +120,7 @@ public function theTokenMustContainTheClaimWithValue($claim, $value) public function theTokenMustContainTheClaim($claim) { /** @var JWTEncoderInterface $encoder */ - $encoder = $this->getContainer()->get('lexik_jwt_authentication.encoder'); + $encoder = $this->getEncoder(); $token_decoded = $encoder->decode($this->getToken()); if (!array_key_exists($claim, $token_decoded)) { @@ -113,7 +134,7 @@ public function theTokenMustContainTheClaim($claim) public function iHaveAValidSignedAndEncryptedToken() { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = $this->getBasicPayload(); $signatureKey = $this->getSignatureKey(); $jwt = $jwsBuilder @@ -126,7 +147,7 @@ public function iHaveAValidSignedAndEncryptedToken() $jws = $serialzer->serialize($jwt); /** @var JWEBuilder $jweBuilder */ - $jweBuilder = $this->getContainer()->get('jose.jwe_builder.lexik_jose'); + $jweBuilder = $this->getJWEBuilder(); $encryptionKey = $this->getEncryptionKey(); $jwe = $jweBuilder ->create() @@ -149,7 +170,7 @@ public function iHaveAValidSignedAndEncryptedToken() public function iHaveASignedAndEncryptedTokenButWithoutTheClaim($claim) { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = $this->getBasicPayload(); unset($payload[$claim]); $signatureKey = $this->getSignatureKey(); @@ -163,7 +184,7 @@ public function iHaveASignedAndEncryptedTokenButWithoutTheClaim($claim) $jws = $serialzer->serialize($jwt); /** @var JWEBuilder $jweBuilder */ - $jweBuilder = $this->getContainer()->get('jose.jwe_builder.lexik_jose'); + $jweBuilder = $this->getJWEBuilder(); $encryptionKey = $this->getEncryptionKey(); $jwe = $jweBuilder ->create() @@ -184,7 +205,7 @@ public function iHaveASignedAndEncryptedTokenButWithoutTheClaim($claim) public function iHaveAnExpiredSignedAndEncryptedToken() { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = array_merge( $this->getBasicPayload(), [ @@ -204,7 +225,7 @@ public function iHaveAnExpiredSignedAndEncryptedToken() $jws = $serialzer->serialize($jwt); /** @var JWEBuilder $jweBuilder */ - $jweBuilder = $this->getContainer()->get('jose.jwe_builder.lexik_jose'); + $jweBuilder = $this->getJWEBuilder(); $encryptionKey = $this->getEncryptionKey(); $jwe = $jweBuilder ->create() @@ -225,7 +246,7 @@ public function iHaveAnExpiredSignedAndEncryptedToken() public function iHaveASignedAndEncryptedTokenButWithWrongIssuer() { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = array_merge( $this->getBasicPayload(), [ @@ -243,7 +264,7 @@ public function iHaveASignedAndEncryptedTokenButWithWrongIssuer() $jws = $serialzer->serialize($jwt); /** @var JWEBuilder $jweBuilder */ - $jweBuilder = $this->getContainer()->get('jose.jwe_builder.lexik_jose'); + $jweBuilder = $this->getJWEBuilder(); $encryptionKey = $this->getEncryptionKey(); $jwe = $jweBuilder ->create() @@ -264,7 +285,7 @@ public function iHaveASignedAndEncryptedTokenButWithWrongIssuer() public function iHaveASignedAndEncryptedTokenButWithWrongAudience() { /** @var JWSBuilder $jwsBuilder */ - $jwsBuilder = $this->getContainer()->get('jose.jws_builder.lexik_jose'); + $jwsBuilder = $this->getJWSBuilder(); $payload = array_merge( $this->getBasicPayload(), [ @@ -282,7 +303,7 @@ public function iHaveASignedAndEncryptedTokenButWithWrongAudience() $jws = $serialzer->serialize($jwt); /** @var JWEBuilder $jweBuilder */ - $jweBuilder = $this->getContainer()->get('jose.jwe_builder.lexik_jose'); + $jweBuilder = $this->getJWEBuilder(); $encryptionKey = $this->getEncryptionKey(); $jwe = $jweBuilder ->create() @@ -318,8 +339,8 @@ private function getBasicPayload() 'iat' => time() - 100, 'nbf' => time() - 100, 'jti' => 'w53JxRXaEwGn80Jb4c-EZieTfvWgZDzhBw4C3Gv_0VId4zj4KaY6ujkDv9C3y7LLj5gSi9JCzfuBR2Km4vBsVA', - 'iss' => $this->getContainer()->getParameter('lexik_jose_bridge.encoder.issuer'), - 'aud' => $this->getContainer()->getParameter('lexik_jose_bridge.encoder.audience'), + 'iss' => $this->getIssuer(), + 'aud' => $this->getAudience(), 'ip' => '127.0.0.1', ]; } @@ -332,7 +353,7 @@ private function getSignatureHeader() $header = [ 'typ' => 'JWT', 'cty' => 'JWT', - 'alg' => $this->getContainer()->getParameter('lexik_jose_bridge.encoder.signature_algorithm'), + 'alg' => $this->getSignatureAlgorithm(), ]; $signatureKey = $this->getSignatureKey(); if ($signatureKey->has('kid')) { @@ -350,8 +371,8 @@ private function getEncryptionHeader() $header = [ 'typ' => 'JWT', 'cty' => 'JWT', - 'alg' => $this->getContainer()->getParameter('lexik_jose_bridge.encoder.encryption.key_encryption_algorithm'), - 'enc' => $this->getContainer()->getParameter('lexik_jose_bridge.encoder.encryption.content_encryption_algorithm'), + 'alg' => $this->getKeyEncryptionAlgorithm(), + 'enc' => $this->getContentEncryptionAlgorithm(), ]; $encryption_key = $this->getEncryptionKey(); if ($encryption_key->has('kid')) { @@ -363,15 +384,19 @@ private function getEncryptionHeader() private function getSignatureKey(): JWK { - $keyIndex = $this->getContainer()->getParameter('lexik_jose_bridge.encoder.key_index'); + $keyIndex = $this->getEncoderKeyIndex(); - return $this->getContainer()->get('jose.key_set.lexik_jose_bridge.signature')->get($keyIndex); + $jwkSet = $this->getJWKSetSignature(); + + return $jwkSet->get($keyIndex); } private function getEncryptionKey(): JWK { - $keyIndex = $this->getContainer()->getParameter('lexik_jose_bridge.encoder.encryption.key_index'); + $keyIndex = $this->getEncoderEncryptionKeyIndex(); + + $jwkSet = $this->getJWKSetEncryption(); - return $this->getContainer()->get('jose.key_set.lexik_jose_bridge.encryption')->get($keyIndex); + return $jwkSet->get($keyIndex); } } diff --git a/Tests/Context/RequestContext.php b/Tests/Context/RequestContext.php index 851afea..c91a790 100644 --- a/Tests/Context/RequestContext.php +++ b/Tests/Context/RequestContext.php @@ -16,12 +16,15 @@ use Behat\Mink\Driver\BrowserKitDriver; use function count; use Exception; +use SpomkyLabs\TestBundle\EventListener\JWTListener; trait RequestContext { private $request_builder; private $exception; + abstract public function getJWTListener(): JWTListener; + /** * @return null|string */ @@ -206,9 +209,9 @@ public function iShouldReceiveAnException($message) */ public function theErrorListenerShouldReceiveAnExpiredTokenEvent() { - $events = $this->getContainer()->get('acme_api.event.jwt_created_listener')->getExpiredTokenEvents(); + $events = $this->getJWTListener()->getExpiredTokenEvents(); if (1 !== count($events)) { - throw new Exception(); + throw new Exception('Expected 1 expired token event, got '.count($events)); } } @@ -217,9 +220,9 @@ public function theErrorListenerShouldReceiveAnExpiredTokenEvent() */ public function theErrorListenerShouldReceiveAnInvalidTokenEvent() { - $events = $this->getContainer()->get('acme_api.event.jwt_created_listener')->getInvalidTokenEvents(); + $events = $this->getJWTListener()->getInvalidTokenEvents(); if (1 !== count($events)) { - throw new Exception(); + throw new Exception('Expected 1 invalid token event, got '.count($events)); } } @@ -230,7 +233,7 @@ public function theErrorListenerShouldReceiveAnInvalidTokenEvent() */ public function theErrorListenerShouldReceiveAnInvalidTokenEventContainingAnExceptionWithMessage($message) { - $events = $this->getContainer()->get('acme_api.event.jwt_created_listener')->getInvalidTokenEvents(); + $events = $this->getJWTListener()->getInvalidTokenEvents(); foreach ($events as $event) { $exception = current($events)->getException(); diff --git a/Tests/app/AppKernel.php b/Tests/app/AppKernel.php index 13940c3..523aecf 100644 --- a/Tests/app/AppKernel.php +++ b/Tests/app/AppKernel.php @@ -24,6 +24,7 @@ public function registerBundles(): array return [ new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\MonologBundle\MonologBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), @@ -32,6 +33,8 @@ public function registerBundles(): array new Jose\Bundle\JoseFramework\JoseFrameworkBundle(), new SpomkyLabs\TestBundle\SpomkyLabsTestBundle(), new SpomkyLabs\LexikJoseBundle\SpomkyLabsLexikJoseBundle(), + + new FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle(), ]; } @@ -40,7 +43,15 @@ public function registerBundles(): array */ public function getCacheDir(): string { - return sys_get_temp_dir().'/SpomkyLabsLexikBridgeTest'; + return sys_get_temp_dir().'/SpomkyLabsLexikBridgeTest/cache'; + } + + /** + * {@inheritdoc} + */ + public function getLogDir(): string + { + return sys_get_temp_dir().'/SpomkyLabsLexikBridgeTest/logs'; } /** diff --git a/Tests/app/autoload.php b/Tests/app/autoload.php index 785707f..822d091 100644 --- a/Tests/app/autoload.php +++ b/Tests/app/autoload.php @@ -17,6 +17,6 @@ // @var ClassLoader $loader = require __DIR__.'/../../vendor/autoload.php'; -//AnnotationRegistry::registerLoader([$loader, 'loadClass']); +// AnnotationRegistry::registerLoader([$loader, 'loadClass']); return $loader; diff --git a/Tests/app/config/config.yml b/Tests/app/config/config.yml index 306f072..bd1bf6b 100644 --- a/Tests/app/config/config.yml +++ b/Tests/app/config/config.yml @@ -9,17 +9,36 @@ framework: form: ~ csrf_protection: ~ session: - storage_id: session.storage.mock_file +# storage_id: session.storage.mock_file + storage_factory_id: session.storage.factory.mock_file router: - resource: '%kernel.root_dir%/config/routing.yml' + resource: '%kernel.project_dir%/Tests/app/config/routing.yml' strict_requirements: ~ - templating: - engines: ['twig'] +# templating: +# engines: ['twig'] trusted_hosts: ~ fragments: ~ http_method_override: true services: + _defaults: + autowire: true + autoconfigure: true + + SpomkyLabs\TestBundle\: + resource: '../../../Tests/Bundle/TestBundle/*' + exclude: + - '../../../Tests/Bundle/TestBundle/Controller/' + + SpomkyLabs\TestBundle\Controller\: + resource: '../../../Tests/Bundle/TestBundle/Controller/*' + tags: ['controller.service_arguments'] + + SpomkyLabs\LexikJoseBundle\Features\Context\: + resource: '../../../Tests/Context/*' + arguments: + - '@behat.driver.service_container' + Psr\EventDispatcher\EventDispatcherInterface: alias: Symfony\Contracts\EventDispatcher\EventDispatcherInterface @@ -29,8 +48,8 @@ twig: strict_variables: true lexik_jwt_authentication: - secret_key: '%kernel.root_dir%/keys/private.key' - public_key: '%kernel.root_dir%/keys/public.key' + secret_key: '%kernel.project_dir%/Tests/app/keys/private.key' + public_key: '%kernel.project_dir%/Tests/app/keys/public.key' lexik_jose: ttl: 1000 @@ -53,3 +72,10 @@ lexik_jose: key_index: "KEY_ID_0" key_encryption_algorithm: 'A256GCMKW' # The key encryption algorithm content_encryption_algorithm: 'A256GCM' # The content encryption algorithm + +monolog: + handlers: + file_log: + type: stream + path: "%kernel.logs_dir%/%kernel.environment%.log" + level: debug \ No newline at end of file diff --git a/Tests/app/config/security.yml b/Tests/app/config/security.yml index 8ead4ec..f08203e 100644 --- a/Tests/app/config/security.yml +++ b/Tests/app/config/security.yml @@ -1,6 +1,7 @@ security: - encoders: - Symfony\Component\Security\Core\User\User: plaintext + enable_authenticator_manager: true + password_hashers: + Symfony\Component\Security\Core\User\InMemoryUser: plaintext role_hierarchy: ROLE_ADMIN: ROLE_USER providers: @@ -17,14 +18,13 @@ security: api: pattern: ^/api stateless: true - anonymous: true - guard: - authenticators: - - lexik_jwt_authentication.jwt_token_authenticator + jwt: ~ +# guard: +# authenticators: +# - lexik_jwt_authentication.jwt_token_authenticator login: pattern: ^/ stateless: true - anonymous: true form_login: login_path: login check_path: login @@ -32,4 +32,4 @@ security: success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure access_control: - - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: ^/login, roles: PUBLIC_ACCESS } diff --git a/behat.yml b/behat.yml index 69f5155..72f56b3 100644 --- a/behat.yml +++ b/behat.yml @@ -6,22 +6,23 @@ default: extensions: # Caciobanu\Behat\DeprecationExtension: # mode: 999999 - Behat\Symfony2Extension: + FriendsOfBehat\SymfonyExtension: kernel: debug: true path: 'Tests/app/AppKernel.php' - bootstrap: 'Tests/app/autoload.php' + class: AppKernel + bootstrap: 'Tests/app/autoload.php' Behat\MinkExtension: show_cmd: firefox %s base_url: 'https://www.example.test/' sessions: - symfony2: - symfony2: ~ + symfony: + symfony: ~ suites: default: paths: - '%paths.base%/Features' contexts: - 'SpomkyLabs\LexikJoseBundle\Features\Context\FeatureContext' - mink_session: 'symfony2' + mink_session: 'symfony' bundle: 'SpomkyLabsLexikJoseBundle' diff --git a/composer.json b/composer.json index 6a7bb14..df65530 100644 --- a/composer.json +++ b/composer.json @@ -2,52 +2,57 @@ "name": "spomky-labs/lexik-jose-bridge", "type": "symfony-bundle", "description": "Bridge to allow the use of web-token/jwt-framework with the Lexik JWT Authentication Bundle", - "keywords": ["Jose", "Bundle", "Symfony"], + "keywords": [ + "Jose", + "Bundle", + "Symfony" + ], "homepage": "https://github.com/Spomky-Labs/lexik-jose-bridge", "license": "MIT", "authors": [ { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" }, { - "name": "All contributors", - "homepage": "https://github.com/Spomky-Labs/lexik-jose-bridge/contributors" + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/lexik-jose-bridge/contributors" } ], "require": { - "web-token/jwt-bundle": "^2.0.3", - "web-token/jwt-checker": "^2.0.3", - "web-token/jwt-signature": "^2.0.3", - "web-token/jwt-signature-algorithm-rsa": "^2.0.3", - "web-token/jwt-encryption": "^2.0.3", - "web-token/jwt-key-mgmt": "^2.0.3", "lexik/jwt-authentication-bundle": "^2.0", - "thecodingmachine/safe": "^0.1.15|^1.0", - "psr/event-dispatcher": "^1.0" + "psr/event-dispatcher": "^1.0", + "thecodingmachine/safe": "^2.0", + "web-token/jwt-bundle": "^3.0", + "web-token/jwt-checker": "^3.0", + "web-token/jwt-encryption": "^3.0", + "web-token/jwt-key-mgmt": "^3.0", + "web-token/jwt-signature": "^3.0", + "web-token/jwt-signature-algorithm-rsa": "^3.0" }, "require-dev": { - "symfony/templating": "^4.3|^5.0", - "symfony/form": "^4.3|^5.0", - "symfony/finder": "^4.3|^5.0", - "symfony/twig-bundle": "^4.3|^5.0", - "symfony/expression-language": "^4.3|^5.0", - "symfony/dependency-injection": "^4.3|^5.0", - "symfony/var-dumper": "^4.3|^5.0", - "sensio/framework-extra-bundle": "^4.0|^5.0", "behat/behat": "^3.0", - "behat/mink-extension": "^2.3", - "behat/mink-browserkit-driver": "^1.3", - "behat/symfony2-extension": "^2.1", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", + "caciobanu/behat-deprecation-extension": "^2.1", + "friends-of-behat/mink-browserkit-driver": "^1.6", + "friends-of-behat/mink-extension": "^2.3", + "friends-of-behat/symfony-extension": "^2.3", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "sensio/framework-extra-bundle": "^6.0", + "symfony/dependency-injection": "^6.0", + "symfony/expression-language": "^6.0", + "symfony/finder": "^6.0", + "symfony/form": "^6.0", + "symfony/monolog-bundle": "^3.7", + "symfony/templating": "^6.0", + "symfony/twig-bundle": "^6.0", + "symfony/var-dumper": "^6.0", "thecodingmachine/phpstan-safe-rule": "^1.0", - "web-token/jwt-encryption-algorithm-aesgcmkw": "^2.0.3", - "web-token/jwt-encryption-algorithm-aesgcm": "^2.0.3", - "web-token/jwt-signature-algorithm-hmac": "^2.0.3", - "phpstan/phpstan-beberlei-assert": "^0.12", - "caciobanu/behat-deprecation-extension": "^2.1" + "web-token/jwt-encryption-algorithm-aesgcm": "^3.0", + "web-token/jwt-encryption-algorithm-aesgcmkw": "^3.0", + "web-token/jwt-signature-algorithm-hmac": "^3.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon.dist similarity index 61% rename from phpstan.neon rename to phpstan.neon.dist index d6ff8a5..0157377 100644 --- a/phpstan.neon +++ b/phpstan.neon.dist @@ -1,15 +1,12 @@ parameters: - level: 7 + level: 8 paths: - ./ - excludes_analyse: + excludePaths: - %currentWorkingDirectory%/Tests - %currentWorkingDirectory%/var - %currentWorkingDirectory%/vendor checkMissingIterableValueType: false - ignoreErrors: - - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface\:\:scalarNode\(\)\.#' - - '#Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition\:\:addDefaultsIfNotSet\(\)\.#' includes: - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/phpstan/phpstan-deprecation-rules/rules.neon