Closed zeromodule closed 1 year ago
That "JWT Token not found" error message is not coming from the bundle, so you have to figure out where it's coming from.
I'd assume on the initial login there is a cookie with the JWT token returned and for some reason that cookie isn't present when the 2fa auth code is passed.
@scheb the problem is that there is no JWT token in the response on login, neither in the body nor in the headers. But it seems logical to me, because all we have in AuthenticationSuccessHandler
is:
return new Response('{"login": "success", "two_factor_complete": false}');
JWT is generated in the decorated handler only. Shouldn't I add it somehow to the response?
Sounds reasonable. Then you'd likely need to have the decorated success handler generate a response. And when 2fa is required, that information somehow needs to be injected into the response so that your frontend understands that it needs to request 2fa code.
@scheb How bundle understands that 2FA is in progress when user is accessing 2fa_login_check
route? Shouldn't this information be stored in JWT?
That information is stored in the security token, which is stored in the session. That's why the firewall needs to be stateful.
@scheb am I right that security token you're talking about must be already stored in the session at the moment of execution of AuthenticationSuccessHandler
?
I checked session status at this moment and it's equals 1, which means PHP_SESSION_NONE. So it haven't been even started.
class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler
{
public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response
{
if ($token instanceof TwoFactorTokenInterface) {
var_dump(\session_status()); // 1
return new Response('{"login": "success", "two_factor_complete": false}');
}
return parent::onAuthenticationSuccess($request, $token);
}
}
At that moment, the session should be definitely active. The session is managed by Symfony, that's likely why you don't get a result from native PHP functions. See https://symfony.com/doc/current/session.html about details on Symfony sessions.
@scheb Thanks a lot, you gave me the right idea. In the API Platform distribution, sessions were disabled by default (session section was commented in the framework.yaml
). After turning them on everything worked. Also, regarding integration with LexikJWT, I just use lexik_jwt_authentication.handler.authentication_success
as value for two_factor.success_handler
, so the client only gets a JWT when two-factor authentication is complete.
Thanks again for the help and for the great bundle!
Below is my final config, it may be useful to someone else.
#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\User
property: email
firewalls:
dev:
pattern: ^/_(profiler|wdt)
security: false
login:
pattern: ^/auth
stateless: false
json_login:
check_path: auth_login
username_path: email
password_path: password
success_handler: App\Security\AuthenticationSuccessHandler
two_factor:
auth_form_path: 2fa_form # The route name you have used in the routes.yaml
check_path: 2fa_check # The route name you have used in the routes.yaml
prepare_on_login: true
prepare_on_access_denied: true
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: App\Security\TwoFactorAuthenticationFailureHandler
api:
pattern: ^/
provider: app_user_provider
stateless: true
jwt: ~
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
- { path: ^/$, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI
- { path: ^/docs, roles: PUBLIC_ACCESS } # Allows accessing the Swagger UI docs
- { path: ^/auth/login, roles: PUBLIC_ACCESS }
- { path: ^/auth/2fa, role: IS_AUTHENTICATED_2FA_IN_PROGRESS }
- { path: ^/, roles: IS_AUTHENTICATED_FULLY }
#routes.yaml
controllers:
resource:
path: ../src/Controller/
namespace: App\Controller
type: attribute
auth_login:
path: /auth/login
methods: [ 'POST' ]
#packages/scheb_2fa.yaml
scheb_two_factor:
security_tokens:
- Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
- Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
- Lexik\Bundle\JWTAuthenticationBundle\Security\Authenticator\Token\JWTPostAuthenticationToken
- Lexik\Bundle\JWTAuthenticationBundle\Security\Authentication\Token\JWTUserToken
email:
enabled: true
sender_email: no-reply@example.com
sender_name: John Doe # Optional
digits: 4
#routes/scheb_2fa.yaml
2fa_form:
path: /auth/2fa_form
defaults:
# "scheb_two_factor.form_controller" references the controller service provided by the bundle.
# You don't HAVE to use it, but - except you have very special requirements - it is recommended.
_controller: "scheb_two_factor.form_controller::form"
2fa_check:
path: /auth/2fa_check
Bundle version: 6.8.0 Symfony version: 6.3.1 PHP version: 8.2.7 Using authenticators (
enable_authenticator_manager: true
): NODescription
Hi, I'm using your bundle with API Platform and LexikJWTAuthenticationBundle. Application is API-only. I successfully receive the codes and correct response by sending username&password to the
/auth
endpoint, but when I try to send the code to/2fa_check
, I always get 401 Unauthorized:Additional Context
security.yaml
packages/scheb_2fa.yaml
routes/scheb_2fa.yaml
packages/lexik_jwt_authentication.yaml
App\Security\AuthenticationSuccessHandler
Other handlers in
App\Security\
are implemented exactly as in https://symfony.com/bundles/SchebTwoFactorBundle/6.x/api.html