lexik/LexikJWTAuthenticationBundle

The algorithm with the alias "" is not supported

NeuralClone opened this issue · 4 comments

Q A
Symfony Version 6.4.7
Bundle Version 3.0.0
PHP Version 8.3.7
Related issues/PRs #1209, #1214

I've been attempting to configure this bundle to use the Web-Token feature as outlined in the documentation and have run into some issues.

I have the following configuration:

lexik_jwt_authentication:
  secret_key: '%env(resolve:JWT_SECRET_KEY)%'
  public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
  pass_phrase: '%env(JWT_PASSPHRASE)%'
  api_platform:
    enabled: true
    check_path: /api/login_check
    username_path: username
    password_path: password
  encoder:
    service: lexik_jwt_authentication.encoder.web_token
  token_ttl: 3600
  allow_no_expiration: false
  clock_skew: 0
  user_id_claim: username
  token_extractors:
    authorization_header:
      enabled: true
      prefix: Bearer
      name: Authorization
    cookie:
      enabled: false
      name: BEARER
    query_parameter:
      enabled: false
      name: bearer
    split_cookie:
      enabled: false
      cookies: {  }
  remove_token_from_body_when_cookies_used: true
  set_cookies: {  }
  access_token_issuance:
    enabled: true
    signature:
      algorithm: RS256
      key: # some key
  access_token_verification:
    enabled: true
    signature:
      allowed_algorithms:
        - RS256
      keyset: # keyset
  blocklist_token:
    enabled: false
    cache: cache.app

If encryption isn't enabled, an InvalidArgumentException is always thrown by AccessTokenBuilder whenever a request for a token is made. For example, the following always fails when encryption isn't enabled:

curl -X POST http://localhost/api/login_check \
-H "Content-Type: application/json" \
-d '{"username":"someusername","password":"somepassword"}'

The problematic lines of code appear to be here:

$this->jwsBuilder = $jwsBuilderFactory->create([$signatureAlgorithm]);
if ($jweBuilderFactory !== null && $keyEncryptionAlgorithm !== null && $contentEncryptionAlgorithm !== null) {
$this->jweBuilder = $jweBuilderFactory->create([$keyEncryptionAlgorithm, $contentEncryptionAlgorithm]);
}

$keyEncryptionAlgorithm and $contentEncryptionAlgorithm aren't automatically set to null if they aren't defined and instead are set to empty strings. This always causes an exception to the thrown due to the encryption algorithm being set to an empty string. If I manually set both variables to null before the above lines of code, my configuration works and a token is correctly sent in the response.

The encryption related service arguments are replaced here, but only if encryption is enabled:

if ($config['access_token_issuance']['encryption']['enabled'] === true) {
$accessTokenBuilderDefinition
->replaceArgument(5, $config['access_token_issuance']['encryption']['key_encryption_algorithm'])
->replaceArgument(6, $config['access_token_issuance']['encryption']['content_encryption_algorithm'])
->replaceArgument(7, $config['access_token_issuance']['encryption']['key'])
;
}
}

Otherwise, they remain as empty strings, which doesn't appear to be the intention since it makes encryption mandatory.

I've attempted to directly set the encryption configuration options to null as a possible workaround, but that causes a different exception to be thrown that says they can't be null. That seems to directly contradict the service responsible for building tokens.

Am I missing an important configuration option/detail that addresses these issues or is this a bug? I've gone through the code and documentation in an attempt to find such an option and haven't had any luck. Is encryption supposed to be required when using the web-token feature?

I was finally able to get back to this today. After doing some additional debugging, the same problem exists in AccessTokenLoader:

$this->jwsLoader = $jwsLoaderFactory->create(['jws_compact'], $signatureAlgorithms, $jwsHeaderChecker);
if ($jweLoaderFactory !== null && $keyEncryptionAlgorithms !== null && $contentEncryptionAlgorithms !== null && $jweHeaderChecker !== null) {
$this->jweLoader = $jweLoaderFactory->create(['jwe_compact'], array_merge($keyEncryptionAlgorithms, $contentEncryptionAlgorithms), null, null, $jweHeaderChecker);
$this->continueOnDecryptionFailure = $continueOnDecryptionFailure;
}

Likewise, the service arguments are set here:

if ($config['access_token_verification']['encryption']['enabled'] === true) {
$accessTokenLoaderDefinition
->replaceArgument(8, $config['access_token_verification']['encryption']['continue_on_decryption_failure'])
->replaceArgument(9, $config['access_token_verification']['encryption']['header_checkers'])
->replaceArgument(10, $config['access_token_verification']['encryption']['allowed_key_encryption_algorithms'])
->replaceArgument(11, $config['access_token_verification']['encryption']['allowed_content_encryption_algorithms'])
->replaceArgument(12, $config['access_token_verification']['encryption']['keyset'])
;
}

The service is expecting the encryption algorithms and associated keysets to be null but are empty strings instead. Therefore, verification always fails.

The easiest solution in both cases would probably be to make sure the appropriate arguments are set to null if they aren't set in the configuration.

Workaround

I've been able to fix both of these issues in my Kernel class by implementing CompilerPassInterface and directly overriding the default values as described in the Symfony documentation: How to Work with Compiler Passes.

// src/Kernel.php
namespace App;

use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;

class Kernel extends BaseKernel implements CompilerPassInterface
{
    use MicroKernelTrait;
    
    // ...

    public function process(ContainerBuilder $container): void
    {
        $accessTokenBuilderService = 'lexik_jwt_authentication.access_token_builder';
        $accessTokenLoaderService = 'lexik_jwt_authentication.access_token_loader';

        if ($container->hasDefinition($accessTokenBuilderService)) {
            $container->getDefinition($accessTokenBuilderService)
                ->replaceArgument(5, null)
                ->replaceArgument(6, null)
                ->replaceArgument(7, null);
        }

        if ($container->hasDefinition($accessTokenLoaderService)) {
            $container->getDefinition($accessTokenLoaderService)
                ->replaceArgument(9, null)
                ->replaceArgument(10, null)
                ->replaceArgument(11, null)
                ->replaceArgument(12, null);
        }
    }

    // ...
}

This doesn't solve the core problem, but it's a reasonable and easy to implement workaround until it is fixed.

Thanks for the detailed report and workaround!
I'm going to look into this asap though you're welcome if you feel like trying to fix the root cause :)

You're welcome! I've created a pull request that addresses the root cause. Setting default values for the problematic arguments in the service configurations and changing the logic to look for empty arrays instead of null in AccessTokenLoader are the only changes that were needed.

This fix was originally suggested in #1209, but it wasn't fully implemented in #1214. Empty strings were set as default values and the logic wasn't updated in AccessTokenLoader to address the empty arrays.

Are there any updates on this? Is there anything you'd like changed/reworked in the pull request I created?

I'm using the workaround I described above in projects that use this bundle and that's continued to work well. So, there's absolutely no rush. But it'd be nice to not have to use a compiler pass to fix the configuration.