scheb / 2fa

Two-factor authentication for Symfony applications 🔐
MIT License
495 stars 72 forks source link

LexikJWTAuthenticationBundle 2FA fails everytime #159

Closed Evoolo closed 1 year ago

Evoolo commented 2 years ago

Bundle version: 5.13.2 Symfony version: 5.4.11 PHP version: 7.4.30 Using authenticators (enable_authenticator_manager: true): YES

Description

I try to use this Bundle together with LexikJWTAuthenticationBundle. I've completed the Installation guide and the API guide. I've also written a small test controller to see if QR code and token verification are OK. But i miss something. When i try to authenticate, the instanceof TwoFactorTokenInterface always fails. I also never get the response from the failed handler or the required handler, only from the success handler:

{ "login": "success": "2fa_complete": false }

It doesn't matter if i provide a valid, invalid or no token. the Response is always the same. Only if i delete the Security Token from DB, i get an JWT Token.

Can help me to find out what i am missing? Thank you!

Additional Context

security.yaml


security:
    enable_authenticator_manager: true
    # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
    # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
    providers:
        app_user_provider:
            entity:
                class: App\Entity\Account\Account
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: true
            stateless: true
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator
        register:
            pattern: ^/api/register #this is regexp, so all urls starting with /public/ will   match
            security: false #this will be public, no firewall
        login:
            pattern: ^/api/login  #this is regexp, so all urls starting with /public/ will   match
            stateless: false
            two_factor:
                prepare_on_login: true
                prepare_on_access_denied: true
                #check_path: api_2fa_login_check
                auth_code_parameter_name: auth_code
                authentication_required_handler: App\Security\TwoFactorAuthenticationRequiredHandler
                success_handler: App\Security\TwoFactorAuthenticationSuccessHandler
                failure_handler: App\Security\TwoFactorAuthenticationFailureHandler
                check_path: 2fa_login_check  # The route name you have used in the routes.yaml
            json_login:
                check_path: /api/login_check # or api_login_check as defined in config/routes.yaml
                username_path: email
                password_path: password
                success_handler: App\Security\CombinedAuthenticationSuccessHandler
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        main:
            lazy: true

            provider: app_user_provider
            custom_authenticators:
                - App\Security\ApiKeyAuthenticator

            logout:
                path: app_logout

CombinedAuthenticationSuccessHandler


<?php

declare(strict_types=1);

namespace App\Security;

use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Security\Http\Authentication\AuthenticationSuccessHandler;

use function json_decode;
use function json_encode;

final class CombinedAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function __construct(AuthenticationSuccessHandler $decoratedAuthenticationSuccessHandler)
    {
        $this->decoratedAuthenticationSuccessHandler = $decoratedAuthenticationSuccessHandler;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token ): Response
    {

        $response = $this->decoratedAuthenticationSuccessHandler->onAuthenticationSuccess($request, $token);

        //return $response;
        if ($token instanceof TwoFactorTokenInterface) {
            // Return the response to tell the client two-factor authentication is required.
            return new Response('{"login": "success": "2fa_complete": false}');
        }
        //$response = $this->decoratedAuthenticationSuccessHandler->onAuthenticationSuccess($request, $token);

        return $response;
    }
}

TwoFactorAuthenticationFailureHandler

<?php

declare(strict_types=1);

namespace App\Security;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;

class TwoFactorAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        // Return the response to tell the client that 2fa failed. You may want to add more details
        // from the $exception.
        return new Response('{"error": "2fa_failed", "2fa_complete": false}');
   }
}

TwoFactorAuthenticationRequiredHandler

<?php

declare(strict_types=1);

namespace App\Security;

use Scheb\TwoFactorBundle\Security\Http\Authentication\AuthenticationRequiredHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class TwoFactorAuthenticationRequiredHandler implements AuthenticationRequiredHandlerInterface
{
    public function onAuthenticationRequired(Request $request, TokenInterface $token): Response
    {
        // Return the response to tell the client that authentication hasn't completed yet and
        // two-factor authentication is required.
        return new Response('{"error": "2FA Authentication Required", "2fa_complete": false}');
    }
}

TwoFactorAuthenticationSuccessHandler

<?php

declare(strict_types=1);

namespace App\Security;

use Scheb\TwoFactorBundle\Security\Authentication\Token\TwoFactorTokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class TwoFactorAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
    {
        return new Response('{"login": "success", "2fa_complete": true}');
   }
}
scheb commented 2 years ago

Please follow the troubleshooting guide: https://symfony.com/bundles/SchebTwoFactorBundle/6.x/troubleshooting.html#two-factor-authentication-form-is-not-shown-after-login

And it would be great if you could post your 2fa bundle config file as well.

Evoolo commented 2 years ago

Here is my bundle config. I will follow your guide tomorrow. Thank you!


# See the configuration reference at https://symfony.com/bundles/SchebTwoFactorBundle/5.x/configuration.html
scheb_two_factor:
    security_tokens:
        - Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken
        - Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
        - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
        # If you're using guard-based authentication, you have to use this one:
        # - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
        # If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
        # - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
    google:
        enabled: true
        server_name: XYZ.io     # Server name used in QR code
        digits: 6                      # Number of digits in authentication code
        window: 1                      # How many codes before/after the current one would be accepted as valid
Evoolo commented 2 years ago

Hello,

thanks again for your help. I have now found out that my error is rather conceptual.

The workflow now looks like this:

  1. I call my normal AuthenticationSuccessHandler, if 2FA is needed, the corresponding message comes up.
  2. / submit my code to the /2fa route, the successful verification is confirmed.

The problem is that my bearer token is generated yes step 1, but I want to pass the token only when 2fa is successful, so i need to save the Token in any form until 2fa is successful to return the token with the 2fa success response.

Where do I store my beraer token until 2fa is successfully? Or is there a way to make the token invalid until 2fa is successful?

My first idea was to encrypt the token, store it in a session variable and after the 2fa authentication decrypt the token from the session variable and send it to the client.

Is this the correct way?

scheb commented 2 years ago

I'm sorry, I can't give you good advice on that. I haven't worked with LexikJWTAuthenticationBundle yet, so I don't know how its authentication mechanism is working.

The best advice that I can give you: Search this repository's issue for "JWTAuthenticationBundle", because other people have been trying the same before. And search GitHub for combinations of JWTAuthenticationBundle and scheb/2fa, maybe you're fining a reference implementation that way.

stale[bot] commented 2 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.