markitosgv/JWTRefreshTokenBundle

Symfony 5.4 error AbstractGuardAuthenticator

alexandre-lxi opened this issue Β· 28 comments

Since Symfony 5.4, I have an error with the bundle.

Attempted to load class "AbstractGuardAuthenticator" from namespace "Symfony\Component\Security\Guard".
Did you forget a "use" statement for another namespace?

class RefreshTokenAuthenticator extends AbstractGuardAuthenticator

Could you help me to update the bundle ?

Ensure you have symfony/security-guard installed. Note, the Security Guard component is deprecated in Symfony 5.4 and isn't supported in Symfony 6.

symfony/security-guard is removed in Symfony 6. What is the workaround for this?

Update your app to use the newer refresh_jwt authenticator.

Already using the the newer refresh_jwt authenticator.

refresh:
pattern: ^/api/token/refresh
stateless: true
refresh_jwt: ~

If you're getting an error about the Symfony\Component\Security\Guard\AbstractGuardAuthenticator class missing, then something's still trying to use it (maybe not this bundle specifically but somewhere in your app). Try to get a full stack trace for the error to figure out what's calling it, that will help greatly in finding the source of the problem.

For this bundle's CI, symfony/security-guard is explicitly removed before running the tests in the Symfony 6 environment. So there is a bit of a sanity check here with the tests to make sure that nothing's trying to use the Security-Guard component in an unsupported environment.

Yes, I traced it is used in controller: gesdinet.jwtrefreshtoken::refresh

gesdinet_jwt_refresh_token:
    path: /api/token/refresh
    controller: gesdinet.jwtrefreshtoken::refresh

Remove the controller: line from the route definition, it’s not needed with the newer authenticator.

Without controller: getting this error:
Unable to find the controller for path "/api/token/refresh". The route is wrongly configured.

You shouldn't need it, https://github.com/markitosgv/JWTRefreshTokenBundle#define-the-refresh-token-route is pretty much the exact same thing I'm saying here with removing the controller key. #255 (comment) is the only other time I've seen that one referenced and there wasn't really a "fix" shared beyond just rebuilding the route configuration.

I'm in the same boat. Had the same initial config (route with controller), removed the controller part and then I get

Unable to find the controller for path "/v1/login/refresh". The route is wrongly configured.

routes:

gesdinet_jwt_refresh_token:
  path: /v1/login/refresh

config:

gesdinet_jwt_refresh_token:
  single_use: false
  ttl: 2592000
  cookie:
    enabled: true
    same_site: strict
    path: /
    domain: '%env(string:resolve:FRONTEND_DOMAIN)%'
    http_only: true
    secure: true
    remove_token_from_body: true

security:

security:
    login_refresh:
      pattern: ^/v1/login/refresh
      stateless: true
      user_checker: Infrastructure\Shared\Security\UserChecker
      refresh_jwt: ~

It looks like everything is OK in my tests, but this error is returned when navigating via the browser.

image

I wanted to see how my API was reacting when I deleted my cookies in the frontend manually. I manually deleted my token and refresh_token cookie, and I guess then the authenticator no longer supports the request.

image

If I remove only the token cookie and keep the refresh_token cookie, the token is correctly refreshed.

Unable to find the controller for path "/api/token/refresh".

path: /v1/login/refresh

Double-check everything in your configuration and whatever's supposed to be calling the refresh endpoint. Your configuration says the path is /v1/login/refresh but something is trying to use the /api/token/refresh path.

Yeah my bad that's a copy paste error. (I edited my message a few times). But I found the error pops up when no refresh token is set in the request.

You shouldn't need it, https://github.com/markitosgv/JWTRefreshTokenBundle#define-the-refresh-token-route is pretty much the exact same thing I'm saying here with removing the controller key. #255 (comment) is the only other time I've seen that one referenced and there wasn't really a "fix" shared beyond just rebuilding the route configuration.

No, It doesn't work without controller parameter

Same problem. I have to dive into sources and here is what I found out. Just leave the explanation and solution here.

As we can see in RefreshTokenAuthenticator supports() method returns false when there is no refresh token in a request:

public function supports(Request $request): bool
{
    return null !== $this->extractor->getRefreshToken($request, $this->options['token_parameter_name']);
}

so when no token present the authenticator will be skipped and router will fallback to gesdinet.jwtrefreshtoken::refresh action.

Bundle's container configuration says that gesdinet.jwtrefreshtoken alias refer to RefreshToken service which is deprecated and depends on AbstractGuardAuthenticator (this class was removed in Symfony 6).

To solve the problem you need simply define your own fallback endpoint. For example:

namespace App\Controller;

class LoginController extends AbstractController
{
    //
    public function refresh(): JsonResponse
    {
        return new JsonResponse(['msg' => 'JWT refresh token not found'], Response::HTTP_UNAUTHORIZED);
    }
    //
}
gesdinet.jwt_refresh_token:
    path:       /token/refresh
    controller: App\Controller\LoginController::refresh

That's the exact scenario #303 fixes.

IΒ΄m using symfony 6 and a I have the same problem Attempted to load class "AbstractGuardAuthenticator" from namespace "Symfony\Component\Security\Guard". Did you forget a "use" statement for another namespace?
Someone know how to fix this error?

The 1.1 release should fix this.

I use the 1.1 release with Symfony 6 and I have this error:

Attempted to load class "AbstractGuardAuthenticator" from namespace "Symfony\Component\Security\Guard".
Did you forget a "use" statement for another namespace?

image

security.yaml:

security:
    enable_authenticator_manager: true

    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        App\Entity\User:
            algorithm: auto

    providers:
        admin:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin:
            pattern: ^/admin
            provider: admin
            entry_point: form_login
            custom_authenticators:
                - App\Security\AdminAuthenticator
            form_login:
                provider: admin
                login_path: admin_login
                check_path: admin_login_check
                failure_path: admin_login
                default_target_path: admin
                use_forward: false
                use_referer: true
                enable_csrf: true
            logout:
                path: admin_logout
                target: admin_login

        login:
            pattern: ^/api/login$
            stateless: true
            json_login:
                check_path: /api/login
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        refresh:
            pattern: ^/api/token/refresh
            stateless: true

        api:
            pattern: ^/api
            stateless: true
            entry_point: jwt
            jwt: ~
            refresh_jwt:
                check_path: /api/token/refresh
            logout:
                path: api_token_invalidate

    access_control:
        - { path: ^/api/token/refresh, roles: PUBLIC_ACCESS }
        - { path: ^/api/docs, roles: ROLE_ADMIN }
        - { path: ^/api/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: IS_AUTHENTICATED_FULLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

routes.yaml:

api_login_check:
    path: /api/login
    methods: ['POST']

controllers:
    resource: ../src/Controller/
    type: annotation

kernel:
    resource: ../src/Kernel.php
    type: annotation

api_refresh_token:
    path: /api/token/refresh
    controller: gesdinet.jwtrefreshtoken::refresh

api_token_invalidate:
    path: /api/token/invalidate

If I remove controller: gesdinet.jwtrefreshtoken::refresh I get this error:

Unable to find the controller for path "/api/token/refresh". The route is wrongly configured.

image

The controller must be.
Maybe you can try to remove gesdinet and install again (Clear cache too).
I have this in my composer.json:
"gesdinet/jwt-refresh-token-bundle": "dev-master",

@blosky01 same problem after cache clear and same after installed "gesdinet/jwt-refresh-token-bundle": "dev-master",

Security.yaml:

refresh_jwt: check_path: /api/auth/refresh provider: app_user_provider switch_user: false


routes.yaml:

api_refresh_token: path: /api/auth/refresh controller: gesdinet.jwtrefreshtoken::refresh


Services.yaml:

App\OpenApi\RefreshTokenDecorator: decorates: 'api_platform.openapi.factory' arguments: [ '@.inner' ]


in src/Entity/OpenApi/RefreshTokenDecorator.php

`<?php
declare(strict_types=1);

namespace App\OpenApi;

use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model;

final class RefreshTokenDecorator implements OpenApiFactoryInterface
{
public function __construct(
private OpenApiFactoryInterface $decorated
) {}

public function __invoke(array $context = []): OpenApi
{
    $openApi = ($this->decorated)($context);
    $schemas = $openApi->getComponents()->getSchemas();

    $schemas['RefreshToken'] = new \ArrayObject([
        'type' => 'object',
        'properties' => [],
    ]);
    $schemas['RefreshCredentials'] = new \ArrayObject([
        'type' => 'object',
        'properties' => [],
    ]);

    $pathItem = new Model\PathItem(
        ref: 'Refresh JWT Token',
        post: new Model\Operation(
            operationId: 'postCredentialsItem',
            tags: ['Refresh Token'],
            responses: [
                '200' => [
                    'description' => 'Get JWT token',
                    'content' => [
                        'application/json' => [
                            'schema' => [
                                '$ref' => '#/components/schemas/RefreshToken',
                            ],
                        ],
                    ],
                ],
            ],
            summary: 'Refresh JWT By Cookies',
            requestBody: new Model\RequestBody(
                description: 'Generate new JWT Token',
                content: new \ArrayObject([
                    'application/json' => [
                        'schema' => [
                            '$ref' => '#/components/schemas/RefreshCredentials',
                        ],
                    ],
                ]),
            ),
        ),
    );
    $openApi->getPaths()->addPath('/api/auth/refresh', $pathItem);

    return $openApi;
}

}`


in src/Entity/RefreshToken

`<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken as BaseRefreshToken;

#[ORM\Table(name: 'refresh_tokens')]
#[ORM\Entity()]
class RefreshToken extends BaseRefreshToken
{
}
`

Maybe this can help you.
if it doesn't work contact me.

@blosky01 thank you

Can you paste your security.yaml please ?

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'
App\Entity\User:
algorithm: auto
cost: 15
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
# users_in_memory: { memory: null }
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
api:
pattern: ^/api
stateless: true
json_login:
check_path: /api/auth/login
username_path: userIdentifier
password_path: password
success_handler: lexik_jwt_authentication.handler.authentication_success
failure_handler: lexik_jwt_authentication.handler.authentication_failure

        refresh_jwt:
            check_path: /api/auth/refresh
            provider: app_user_provider
        switch_user: false

access_control:
    - { path: ^/api/auth/(login|refresh), roles: PUBLIC_ACCESS }
    - { path: ^/api, roles: PUBLIC_ACCESS }
    - { path: ^/, roles: PUBLIC_ACCESS }

when@test:
security:
password_hashers:
# By default, password hashers are resource intensive and take time. This is
# important to generate secure password hashes. In tests however, secure hashes
# are not important, waste resources and increase test times. The following
# reduces the work factor to the lowest possible values.
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
algorithm: auto
cost: 4 # Lowest possible value for bcrypt
time_cost: 3 # Lowest possible value for argon
memory_cost: 10 # Lowest possible value for argon

@blosky01 Thank you (it's difficult to read your security.yaml ^^). I think it works, when I make a POST request to /api/token/refresh, the results contains a token and refresh_token ?

image

security.yaml:

security:
    enable_authenticator_manager: true

    password_hashers:
        Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
        App\Entity\User:
            algorithm: auto

    providers:
        user:
            entity:
                class: App\Entity\User
                property: email

    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin:
            pattern: ^/admin
            provider: user
            entry_point: form_login
            custom_authenticators:
                - App\Security\AdminAuthenticator
            form_login:
                provider: user
                login_path: admin_login
                check_path: admin_login_check
                failure_path: admin_login
                default_target_path: admin
                use_forward: false
                use_referer: true
                enable_csrf: true
            logout:
                path: admin_logout
                target: admin_login

        api:
            pattern: ^/api
            stateless: true
            json_login:
                check_path: /api/login
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            entry_point: jwt
            jwt: ~
            refresh_jwt:
                check_path: /api/token/refresh
                provider: user
            switch_user: false
            logout:
                path: api_token_invalidate

    access_control:
        - { path: ^/api/token/refresh, roles: PUBLIC_ACCESS }
        - { path: ^/api/docs, roles: ROLE_ADMIN }
        - { path: ^/api/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin/login, roles: PUBLIC_ACCESS }
        - { path: ^/admin, roles: IS_AUTHENTICATED_FULLY }
        - { path: ^/api, roles: IS_AUTHENTICATED_FULLY }

routes.yaml:

api_login_check:
    path: /api/login
    methods: ['POST']

controllers:
    resource: ../src/Controller/
    type: annotation

kernel:
    resource: ../src/Kernel.php
    type: annotation

api_token_refresh:
    path: /api/token/refresh
    controller: gesdinet.jwtrefreshtoken::refresh

api_token_invalidate:
    path: /api/token/invalidate

Sorry for my security.yamlπŸ˜…
I hope I was able to help you solve you problem!πŸ€œπŸΌπŸ€›πŸΌ

No problem, thank you! πŸ‘ŒπŸ™‚

Remove the controller: gesdinet.jwtrefreshtoken::refresh config from the route. That line is only required for folks using Symfony 4.4 applications and will break a Symfony 6 application because the Security-Guard component is not supported on Symfony 6.

@mbabker Great, thanks!)