LexikJWTAuthenticationBundle 2FA fails everytime
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}');
}
}
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.
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
Hello,
thanks again for your help. I have now found out that my error is rather conceptual.
The workflow now looks like this:
- I call my normal AuthenticationSuccessHandler, if 2FA is needed, the corresponding message comes up.
- / 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?
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.
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.