Search code examples
phpsymfonyjwtlexikjwtauthbundle

LexikJWTAuthenticationBundle always rejects JWT tokens when using web-token encoder


I've been attempting to configure LexikJWTAuthenticationBundle for use in a project based on API Platform.

This API is being added to an existing project that uses Sulu CMS (2.6.2) and Symfony (6.4.7). I'm using PHP 8.3 and have OpenSSL installed and enabled.

I'd like to be able to use the LexikJWTAuthenticationBundle's web-token feature. I've been able to get the following configuration, which is based on the bundle's defaults, to work:

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.lcobucci
    signature_algorithm: ES512
  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: {  }

I've followed the documentation for configuring the web-token feature. Unfortunately, everything blows up once I switch over to using lexik_jwt_authentication.encoder.web_token. JWT tokens are always invalid.

I've written my own user provider service to avoid conflicts with Sulu and have verified that it works with the bundle's default encoder.

My bundle configuration after switching to the web-token encoder:

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
    signature_algorithm: ES512
  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: ES512
      key: # key
  access_token_verification:
    enabled: true
    signature:
      allowed_algorithms:
        - ES512
      keyset: # keyset
      header_checkers: {  }
      claim_checkers:
        - exp_with_clock_skew
        - iat_with_clock_skew
        - nbf_with_clock_skew
      mandatory_claims: {  }
  blocklist_token:
    enabled: false
    cache: cache.app

How I'm requesting a token:

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

The request I'm making after receiving a token:

curl -H 'Accept: application/ld+json' \
-H 'Authorization: Bearer <token>' http://localhost/api/some-endpoint/4

I've been able to track the issue down to the stage where the token is loaded and decoded. It's correctly unserialized and broken down into an array. It fails after that point. This makes me suspect there's a problem with the encoding for my keys, but I'm not sure what I'm doing wrong.

I've used the web-token/jwt-bundle's commands to generate keys and I've tried using the standalone JWT app. I've analyzed the results with the bundle's key:analyze and keyset:analyze commands. They appear to be correct. And I've looked at related questions on here to see if someone else has had a similar problem. Unfortunately, that hasn't helped.

I'd greatly appreciate any insight anyone might have on this. I've definitely hit a wall. Thanks in advance!

EDIT: After some more debugging, it looks like encryption is mandatory for this bundle if using the web-token feature even though the documentation says otherwise. Since I didn't have encryption configured, all of my requests using the token were being rejected due to an undefined algorithm.

Encryption is working but decryption is not. It gets to the decryption part of the process and then fails. So, I'm still getting an Invalid JWT Token response.

My full configuration with encryption enabled:

lexik_jwt_authentication:
  secret_key: '%env(resolve:JWT_PRIVATE_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
    signature_algorithm: ES512
  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: ES512
      key: # key
    encryption:
      enabled: true
      key_encryption_algorithm: RSA-OAEP-256
      content_encryption_algorithm: A256CBC-HS512
      key: # key
  access_token_verification:
    enabled: true
    signature:
      allowed_algorithms:
        - ES512
      keyset: # keyset
      header_checkers: {  }
      claim_checkers:
        - exp_with_clock_skew
        - iat_with_clock_skew
        - nbf_with_clock_skew
      mandatory_claims: {  }
    encryption:
      enabled: true
      continue_on_decryption_failure: false
      header_checkers:
        - iat_with_clock_skew
        - nbf_with_clock_skew
        - exp_with_clock_skew
      allowed_key_encryption_algorithms:
        - RSA-OAEP-256
      allowed_content_encryption_algorithms:
        - A256CBC-HS512
      keyset: # keyset
  blocklist_token:
    enabled: false
    cache: cache.app

Solution

  • When I initially posted this question, I was unsure if the issue I was having was a bug or a configuration problem on my end. I had opened an issue in the official repository as well.

    Today I was able to dig into this some more and it does indeed seem to be a bug. I posted a workaround in the GitHub issue along with additional information. But I'll post it here as well for the sake of completeness for anyone who just wants a working solution without needing additional details.

    As a workaround until the bug is fixed, I extended my Kernel class by implementing CompilerPassInterface as explained in the Symfony documentation How to Work with Compiler Passes. Overriding the default encryption configuration values with null prevents exceptions from being thrown and allows both access token issuance and verification to work correctly.

    Note that this workaround only works if you aren't using encryption. I still haven't been able to get decryption working. But that's a separate issue.

    // 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);
            }
        }
    
        // ...
    }
    

    The following example configuration works with the above Kernel implementation. Change as needed.

    # config/services.yaml
    parameters:
      # ...
      env(SIGNATURE_KEY): '%kernel.project_dir%/config/jwt/signature.jwk'
      env(SIGNATURE_KEYSET): '%kernel.project_dir%/config/jwt/signature.jwkset'
    
    # config/packages/lexik_jwt_authentication.yaml
    lexik_jwt_authentication:
      secret_key: '%env(resolve:JWT_PRIVATE_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
      access_token_issuance:
        enabled: true
        signature:
          algorithm: RS256
          key: '%env(file:SIGNATURE_KEY)%'
      access_token_verification:
        enabled: true
        signature:
          allowed_algorithms:
            - RS256
          keyset: '%env(file:SIGNATURE_KEYSET)%'
          header_checkers: {  }
          claim_checkers:
            - exp_with_clock_skew
            - iat_with_clock_skew
            - nbf_with_clock_skew
          mandatory_claims: {  }
      blocklist_token:
        enabled: false
        cache: cache.app