scheb / 2fa

Two-factor authentication for Symfony applications 🔐
MIT License
505 stars 75 forks source link

Error about "two-factor authentication is not in progress" #174

Closed danielrhodeswarp closed 1 year ago

danielrhodeswarp commented 1 year ago

Bundle version: 5.13.2 Symfony version: 4.4.49 PHP version: 7.4.33 Using authenticators (enable_authenticator_manager: true): YES / NO

Description

Trying to integrate Scheb 2FA "on top of" my existing Lexik JWT integration. This is for an app split into a Symfony API back-end and a JavaScript front-end.

Lexik itself is working standardly and hasn't been customised. I POST email and password credentials to the endpoint and this either fails or works. The front-end knows what to do in either case.

I'm following this page of the official bundle docs to complete my integration: https://symfony.com/bundles/SchebTwoFactorBundle/current/api.html

I am able to override Lexik's regular success handler to send a packet like {"login": "success": "2fa_complete": false} to the front-end. This triggers the "You need to complete 2FA" form.

My issue after this is that, when submitting the user's one-time code to the configured /2fa_check endpoint for Scheb 2FA, I hit a 401 error: "Tried to perform two-factor authentication, but two-factor authentication is not in progress".

What causes this error is the $token being null in TwoFactorListener::authenticate():

public function authenticate(RequestEvent $event): void
    {
        // When the firewall is lazy, the token is not initialized in the "supports" stage, so this check does only work
        // within the "authenticate" stage.
        $token = $this->tokenStorage->getToken();

        if (!($token instanceof TwoFactorTokenInterface) || $token->getProviderKey(true) !== $this->twoFactorFirewallConfig->getFirewallName()) {
            // This should only happen when the check path is called outside of a 2fa process and not protected via access_control
            // or when the firewall is configured in an odd way (different firewall name)
            throw new AuthenticationServiceException('Tried to perform two-factor authentication, but two-factor authentication is not in progress.');
        }

        $response = $this->attemptAuthentication($event->getRequest(), $token);
        $event->setResponse($response);
    }

Is this something dopey and obvious that I've missed? Or is it something more low-level and sinister? I'm not super knowledgeable about Symfony under the hood but I'm thinking things like:

[] Do I need to store the temporary token ("valid user but not completed 2FA") on the front-end and send that with the request to /2fa_check ?

[] Should my Symfony be doing the above automatically (cookies?) but my configuration is borked ?

It seems like my subsequent request to check the 2FA code isn't being recognised as belonging to the just-authenticated user.

Additional Context

SECURITY.YAML

# the main event!
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        login:
            pattern:  ^/authenticate
            stateless: false
            #true or 'lazy'?
            anonymous: lazy
                #means our Lexik JWT
            json_login:
                username_path: email
                check_path: api_login_check    #/authenticate/login_check
                success_handler: App\Security\JWTAuthenticationSuccessHandler
                #success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                #Scheb 2FA
            two_factor:
                prepare_on_login: true
                prepare_on_access_denied: true
                auth_form_path: 2fa_login    #/authenticate/2fa
                check_path: 2fa_login_check    #/authenticate/2fa_check
                post_only: true 
                authentication_required_handler: App\Security\TwoFactorAuthenticationRequiredHandler
                success_handler: App\Security\TwoFactorAuthenticationSuccessHandler
                failure_handler: App\Security\TwoFactorAuthenticationFailureHandler
                auth_code_parameter_name: oneTimeCode

        api:
            pattern: ^/
            stateless: false
            provider: app_user_provider
            guard:
                authenticators:
                    - app.jwt_token_authenticator
    access_control:
        - { path: ^/authenticate/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
        - { path: ^/authenticate, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/logout, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/, roles: IS_AUTHENTICATED_FULLY }
danielrhodeswarp commented 1 year ago

Yup, this issue was due to my JavaScript front-end not catching, and re-sending to /2fa_check, the intermediary "logged in but still need 2FA" cookie that Scheb 2FA and Symfony dutifully send back to me when I pass the first traditional sign-in attempt.

Although I am very interested in any ways to send, catch and re-send the intermediary token that aren't cookies :-D

So don't mind me any more, nothing to see here folks.

It's a very interesting bundle, many thanks.

scheb commented 1 year ago

So I guess this can be closed now