Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return value of Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\JwtTokenEncoder::generateToken() must be an instance of Lcobucci\JWT\Token\Plain, instance of Lcobucci\JWT\Token returned #57

Closed
fyrye opened this issue Feb 10, 2021 · 5 comments
Labels

Comments

@fyrye
Copy link

fyrye commented Feb 10, 2021

Bundle version: v5.4.2
Symfony version: 4.4.19
PHP version: 7.3.22
Using authenticators (enable_authenticator_manager: true): NO

Description
When attempting to sign in after supplying the token I receive the error on the 2fa_check route.

Return value of Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\JwtTokenEncoder::generateToken() must be an instance of Lcobucci\JWT\Token\Plain, instance of Lcobucci\JWT\Token returne

scheb_two_factor.yaml

scheb_two_factor:
    trusted_device:
        enabled: true # If the trusted computer feature should be enabled
        cookie_name: trusted_device  # Name of the trusted device cookie
        extend_lifetime: false
        lifetime: 43200 # Lifetime of the trusted computer cookie
        cookie_secure: true # Set the 'Secure' (HTTPS Only) flag on the trusted_computer cookie
        cookie_same_site: strict # The same-site option of the cookie, can be "lax" or "strict"

    email:
        enabled: true # If email authentication should be enabled, default false
        mailer: App\Security\AuthCodeMailer # Use alternative service to send the authentication code
        sender_email: email@domain.com # Sender email address
        sender_name: Me # Sender name
        digits: 6 # Number of digits in authentication code
        template: '@App/security/authentication-form.html.twig'

    totp:
        enabled: true
        issuer: Me
        window: 1
        template: '@App/security/authentication-form.html.twig'

    security_tokens:
        - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken

    ip_whitelist:
        - 127.0.0.1 # localhost IPv4
        - 192.168.0.0/16 # Facility private IPv4 subnet

security.yaml

security:
    encoders:
        App\Entity\User: bcrypt

    providers:
        in_memory:
            memory: ~

        app_user_provider:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev: #should not be affected by existing files
            pattern: ^/(_(profiler|wdt|error)|css|images|js)/
            security: false

        public: #prevents redirect to login
            pattern: ^(?!/order/|/settings/users/employee/\d+/qr-code).*\.(png|pdf)$|^/(about|css|images|js|mediaassets)|^/Scripts/.*\.(^php)$
            security: false

        main:
            anonymous: lazy
            user_checker: App\Security\UserChecker
            provider: app_user_provider
            switch_user: true
            logout:
                path:   /logout
                target: /login
                invalidate_session: true
            guard:
                authenticators:
                    - App\Security\LoginAuthenticator
            two_factor:
                auth_form_path: /2fa_login
                check_path: /2fa_check
                default_target_path: login_redirect #Where to redirect by default after successful authentication
                always_use_default_target_path: true #If it should always redirect to default_target_path
                auth_code_parameter_name: _auth_code
                trusted_parameter_name: _trusted
                multi_factor: false
                enable_csrf: true
                post_only: true

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    access_control:
        - { path: ^/login/redirect, roles: IS_AUTHENTICATED_FULLY } #used to prevent requested pages from being loaded before two-factor
        - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/about, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/2fa, roles: IS_AUTHENTICATED_2FA_IN_PROGRESS }
        - { path: ^/logout, roles: [IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_2FA_IN_PROGRESS] } #allow logged in users and those currently authenticating to logout
        - { path: ^/, roles: ROLE_USER }

To Reproduce

Steps to reproduce the behavior:

  1. Go to /login
  2. Provide Username and Password Credentials and click submit
  3. Prompted for 2FA Code at /2fa_login
  4. Check Email and receive 2FA code
  5. Supply 2FA code into form field and click Submit
  6. See Error above at /2fa_check.

Additional Context
Looking at the composer dependencies for scheb/2fa-trusted-device, it appears that the dependency signatures were changed in lcobucci/jwt version from 3.4 to 4.0 which requires php ^7.4 | ^8.0.

So the composer dependencies for scheb/2fa-trusted-device need to be updated to lcobucci/jwt: ^4.0

https://github.com/lcobucci/jwt/blob/4.0.0/src/Builder.php#L76

   public function getToken(Signer $signer, Key $key): Plain;

https://github.com/lcobucci/jwt/blob/3.4/src/Builder.php#L521

    /**
     * Returns the resultant token
     *
     * @return Token
     */
    public function getToken(Signer $signer = null, Key $key = null)
@fyrye fyrye added the Bug label Feb 10, 2021
@scheb
Copy link
Owner

scheb commented Feb 10, 2021

Which version of lcobucci/jwt do you have installed? composer show --installed should tell you.

Also a stack trace of the exception might be helpful.

Thanks!

@fyrye
Copy link
Author

fyrye commented Feb 11, 2021

@scheb As I have PHP 7.3.22 composer installed lcobucci/jwt 3.4.4, since lcobucci/jwt 4.0+ requires PHP 7.4+

Note this issue also affects scheb/2fa-trusted-device ^5.3 when the JwtTokenEncoder::generateToken return signature was changed to Plain.

Stack Trace (scheb/2fa-bundle 5.4.2)

TypeError:
Return value of Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\JwtTokenEncoder::generateToken() must be an instance of Lcobucci\JWT\Token\Plain, instance of Lcobucci\JWT\Token returned

  at vendor/scheb/2fa-trusted-device/Security/TwoFactor/Trusted/JwtTokenEncoder.php:45
  at Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\JwtTokenEncoder->generateToken('user@domain.com', 'main', 4, object(DateTimeImmutable))
     (vendor/scheb/2fa-trusted-device/Security/TwoFactor/Trusted/TrustedDeviceTokenEncoder.php:33)
  at Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\TrustedDeviceTokenEncoder->generateToken('user@domain.com', 'main', 4)
     (vendor/scheb/2fa-trusted-device/Security/TwoFactor/Trusted/TrustedDeviceTokenStorage.php:89)
  at Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\TrustedDeviceTokenStorage->addTrustedToken('user@domain.com', 'main', 4)
     (var/cache/dev/ContainerO5l8XgX/srcApp_KerneldevDebugContainer.php:30895)
  at ContainerO5l8XgX\TrustedDeviceTokenStorage_fc7b3c4->addTrustedToken('user@domain.com', 'main', 4)
     (vendor/scheb/2fa-trusted-device/Security/TwoFactor/Trusted/TrustedDeviceManager.php:38)
  at Scheb\TwoFactorBundle\Security\TwoFactor\Trusted\TrustedDeviceManager->addTrustedDevice(object(Users), 'main')
     (vendor/scheb/2fa-bundle/Security/Http/Firewall/TwoFactorListener.php:193)
  at Scheb\TwoFactorBundle\Security\Http\Firewall\TwoFactorListener->onSuccess(object(Request), object(PostAuthenticationGuardToken), object(TwoFactorToken))
     (vendor/scheb/2fa-bundle/Security/Http/Firewall/TwoFactorListener.php:150)
  at Scheb\TwoFactorBundle\Security\Http\Firewall\TwoFactorListener->attemptAuthentication(object(Request), object(TwoFactorToken))
     (vendor/scheb/2fa-bundle/Security/Http/Firewall/TwoFactorListener.php:134)
  at Scheb\TwoFactorBundle\Security\Http\Firewall\TwoFactorListener->authenticate(object(RequestEvent))
     (vendor/symfony/security-bundle/Debug/WrappedLazyListener.php:49)
  at Symfony\Bundle\SecurityBundle\Debug\WrappedLazyListener->authenticate(object(RequestEvent))
     (vendor/symfony/security-http/Firewall/AbstractListener.php:27)
  at Symfony\Component\Security\Http\Firewall\AbstractListener->__invoke(object(RequestEvent))
     (vendor/symfony/security-bundle/Security/LazyFirewallContext.php:64)
  at Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext->__invoke(object(RequestEvent))
     (vendor/symfony/security-bundle/Debug/TraceableFirewallListener.php:59)
  at Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener->callListeners(object(RequestEvent), object(Generator))
     (vendor/symfony/security-http/Firewall.php:98)
  at Symfony\Component\Security\Http\Firewall->onKernelRequest(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/Debug/WrappedListener.php:126)
  at Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(object(RequestEvent), 'kernel.request', object(TraceableEventDispatcher))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:264)
  at Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.request', object(RequestEvent))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:239)
  at Symfony\Component\EventDispatcher\EventDispatcher->callListeners(array(object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener), object(WrappedListener)), 'kernel.request', object(RequestEvent))
     (vendor/symfony/event-dispatcher/EventDispatcher.php:73)
  at Symfony\Component\EventDispatcher\EventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php:168)
  at Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch(object(RequestEvent), 'kernel.request')
     (vendor/symfony/http-kernel/HttpKernel.php:134)
  at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), 1)
     (vendor/symfony/http-kernel/HttpKernel.php:80)
  at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), 1, true)
     (vendor/symfony/http-kernel/Kernel.php:201)
  at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
     (/home/app/index.php:22)                

@scheb
Copy link
Owner

scheb commented Feb 11, 2021

lcobucci/jwt 3.4.* contains a compatibility layer for version lcobucci/jwt 4.*, so it is in fact possible to call the library with the signatures from version 4, even when you are using 3.4.*. That's the reason why the signatures have changed, to make the bundle compatible with both versions.

Just from the perspective of the code I cannot really trace down the issue. In v3.4.* Lcobucci\JWT\Token is an alias for Lcobucci\JWT\Token\Plain as seen here https://github.com/lcobucci/jwt/blob/3.4.4/src/Token/Plain.php so you shouldn't get that TypeError. Also, according to unit and integration tests everything is fine. Not sure what I'm missing here.

I have to try and setup a test environment with exactly those versions, see if I can reproduce the issue.

@scheb
Copy link
Owner

scheb commented Feb 11, 2021

I forced the integration test suite to execute with PHP7.3 and got almost the same environment as yours:

https://github.com/scheb/2fa/runs/1881875528?check_suite_focus=true

PHP 7.3.27 and if you look into "Display dependency versions":

lcobucci/jwt                         3.4.4
symfony/symfony                      v4.4.19

The integration test suite is definitly exection that piece of code, but no issue.

I have the suspicion this has something to do with class autoloading and was actually able to reproduce the exception by generating the Composer autoloader with composer dump-autoload --classmap-authoritative. The classmap-authoritative is likely becoming the issue here, because Lcobucci\JWT\Token is not an actual class but an alias in that version of lcobucci/jwt. I believe that's why it's missing in the generated autoloader classmap, therefore the file creating the alias is never loaded, therefore you get a TypeError.

Did you use the classmap-authoritative option in the environment having that issue? If so, I'd say this is actually an issue of the lcobucci/jwt library.

@fyrye
Copy link
Author

fyrye commented Feb 11, 2021

@scheb thanks for looking into the issue more in-depth.
Yes, classmap-authoritative: true is set in composer.json., as recommended by Symfony along with optimize-autoloader: true.

Due to the issue being caused by the return type-hint conflict not loading aliases when using classmap-authoritative, I recommend scheb/2fa-trusted-device:^5.3 require lcobucci/jwt:^4.0 to ensure the issue is not encountered by others in production environments with the same configuration.

To circumvent the issue I updated my composer.json autoload definitions until I can find a better solution, thank you for tracking the root cause down.

    "autoload": {
        "files": [
            "vendor/lcobucci/jwt/src/Token/Plain.php",
            "vendor/lcobucci/jwt/src/Token/Signature.php"
        ],
    },

For clarification, looking at vendor/lcobucci/jwt/compat/class-aliases.php, I do not think this is configured correctly and would need to be addressed by them.

class_exists(\Lcobucci\JWT\Token\Plain::class);
class_exists(\Lcobucci\JWT\Token\Signature::class);

The file is referenced in the autoload configuration vendor/lcobucci/jwt/composer.json

    "autoload": {
        "psr-4": {
            "Lcobucci\\JWT\\": "src"
        },
        "files": [
            "compat/class-aliases.php",
            "compat/json-exception-polyfill.php",
            "compat/lcobucci-clock-polyfill.php"
        ]
    },

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants