markitosgv / JWTRefreshTokenBundle

Implements a Refresh Token system over Json Web Tokens in Symfony
MIT License
663 stars 158 forks source link

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

Open Bohilc opened 2 years ago

Bohilc commented 2 years ago

In symfony 6 refresh token not working properly because it unable to find the controller. Do you have any solution for this case ?

security.yaml -> firewalls:

# ...
 firewalls:
        api_token_refresh:
            pattern: ^/api/token/refresh
            stateless: true
            refresh_jwt: ~

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

        login:
            pattern: ^/api/login
            stateless: true
            json_login:
                check_path: /api/login
                username_path: uusr_login
                password_path: uusr_pass
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

        api:
            pattern: ^/api/
            stateless: true
            provider: app_user_provider
            jwt: ~

        main:
            provider: app_user_provider
            logout:
                path: app_logout
# ...

router.yaml:


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

gesdinet_jwt_refresh_token:
    path: /api/token/refresh
mv-developer commented 2 years ago

You need to add provider to api_token_refresh

Ex:

api_token_refresh:
            pattern: ^/api/token/refresh
            stateless: true
            provider: app_user_provider
            refresh_jwt: ~
matmkian commented 2 years ago

Same issue here, the comment from @mv-developer didn't solved the problem...

DougHayward commented 2 years ago

Same issue here.

gregurco commented 2 years ago

Same issue. Did someone get the idea how to fix it?

andrconstruction commented 2 years ago

Same issue on 5.4

Jayfrown commented 2 years ago

This happens because in the latest release the authenticator only reports support for requests where the refresh token is present. A fix has been merged (just now) but has not yet been included in a release.

So sending an empty request will result in a 404, but using the route as intended (ie. sending the refresh token with the request in a manner the extractor can find it) should work as expected.

Until the next release, a dirty work-around could be to add the following to your route configuration:

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

In this case, using the route as intended will still trigger the authenticator system (and not use the defined controller) but any request to the same path where the authenticator does not trigger, will end up using the controller instead of returning a 404. Just don't forget to remove the controller config when a new version is released.

See 1 and 2.

AimSai59 commented 2 years ago

after a LOT of debugging, I found that the solution is to simply add check_path: gesdinet_jwt_refresh_token in the security.yaml, like this:

firewalls:
    refresh:
        // ...
        refresh_jwt: 
            check_path: gesdinet_jwt_refresh_token
youassi commented 2 years ago

Work well with configuration :

    refresh_jwt:
        refresh_jwt:
            check_path: /api/token/refresh
        provider:  app_user_provider
irmantas commented 2 years ago

If anyone have route issues configuring this with SF 6.1, here is my solution: Add users provider to refresh firewall, configure new route with different path than /api/token/refresh. For example:

# security.yaml
    firewalls:
        # ...
        refresh:
            provider: my_user_provider
            refresh_jwt:
                check_path: api_refresh_token
# routes.yaml
api_refresh_token:
    path: /api/token/renew
fico7489 commented 1 year ago

@youassi that is working, thanks!

mouataz1 commented 1 year ago

hello, i dont know if u still need the solution , any way this ts my configuration and it works jut fine : symfony 6.2 on the routes.yaml file :

api_refresh_token: path: /api/token/refresh

on the security.yaml file this is my firewall :

firewalls:
    dev:
        pattern: ^/(_(profiler|wdt)|css|images|js)/
        security: false
    login:
        pattern: ^/api/login
        stateless: true
        json_login:
            check_path: /api/login_check
            success_handler: lexik_jwt_authentication.handler.authentication_success
            failure_handler: lexik_jwt_authentication.handler.authentication_failure
        logout:
            path: /logout
    api_token_refresh:
        pattern: ^/api/token/refresh
        stateless: true
        refresh_jwt:
            check_path: /api/token/refresh
    api:
        pattern: ^/api
        stateless: true
        jwt: ~
    main:
        custom_authenticators:
            - App\Security\MyGoogleAuthenticator
            - App\Security\MyLinkedinAuthenticator
        entry_point: App\Security\MyGoogleAuthenticator

also do not forget to add the access control :

SergeMezui16 commented 1 year ago

Hi !

I recently had the same problem with Symfony 6.2 and PHP 8.1.

Fortunately, I got the solution.

It seems that the refresh route has been configured to accept only some pattern of url. I test some url for this route and the controller was not the problem. The problem came from the path of our routes. By the look of it, routes path like /auth/refresh/token are bad where routes path like api/refresh/token (whith is recommended by the bundle in doc), refresh/token or token/refresh are good !

I don't take more time to test which path occur an error or not but I think the problem is that the bundle auto configure some routes path and not others.

That is my configuration :

# route.yaml
# ...
api_refresh_token:
    path: /token/refresh
    methods: [POST]
# ...
# security.yaml
security:
# ...
    firewalls:
    # ...
        api_token_refresh:
            pattern: /token/refresh
            stateless: true
            provider: app_user_provider # provider in database
            refresh_jwt:
                check_path: api_refresh_token
    # ...
# ...
    access_control:
        # ...
        - { path: ^/token/refresh$, roles: PUBLIC_ACCESS }
        # ...
# ...

I have removed the path configuration in config/routes/gesdinet_jwt_refresh_token.yaml created by Symfony Recipes, with this configuration it will not work again. I recommend you to remove this file because it isn't helpful anymore. Then, my configuration is like this :

# gesdinet_jwt_refresh_token:
    # path: /token/refresh
# ...

Now, it works so good ! So. That solution is just temporary, I guest they will fix it. I wish I helped you. 👍🏾

maximanek commented 1 year ago

Hello i've done my configuration like @SergeMezui16. And its working just fine, but when I add prefix /api/ to match rest of my app i still got wrongly configured route exception

jay0x commented 1 year ago

Hi guys. I tried to follow @SergeMezui16 instructions... and it didn't work :( I'm getting Unable to find the controller for path "/token/refresh" The route is wrongly configured.

PHP 8.2.6 api-platform v3.1.12 symfony 6.2 lexik/jwt-authentication-bundle v2.19.0 gesdinet/jwt-refresh-token-bundle v1.1.1

Fortunately, I got the solution too :)

security.yaml

    # ...
    firewalls:
        # ...
        main:
            json_login:
               # ...
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            # ...
            refresh_jwt:
                check_path: api_refresh_token
            entry_point: 'lexik_jwt_authentication.security.authentication.entry_point'

routes.yaml

# ...
api_refresh_token:
    path: /api/token/refresh
    methods: [POST]

File config/routes/gesdinet_jwt_refresh_token.yaml was deleted

EnterVPL commented 1 year ago

@SergeMezui16 Very very thanks! ❤️ Your solution works! 😀

Hi !

I recently had the same problem with Symfony 6.2 and PHP 8.1.

Fortunately, I got the solution.

It seems that the refresh route has been configured to accept only some pattern of url. I test some url for this route and the controller was not the problem. The problem came from the path of our routes. By the look of it, routes path like /auth/refresh/token are bad where routes path like api/refresh/token (whith is recommended by the bundle in doc), refresh/token or token/refresh are good !

I don't take more time to test which path occur an error or not but I think the problem is that the bundle auto configure some routes path and not others.

That is my configuration :

# route.yaml
# ...
api_refresh_token:
    path: /token/refresh
    methods: [POST]
# ...
# security.yaml
security:
# ...
    firewalls:
    # ...
        api_token_refresh:
            pattern: /token/refresh
            stateless: true
            provider: app_user_provider # provider in database
            refresh_jwt:
                check_path: api_refresh_token
    # ...
# ...
    access_control:
        # ...
        - { path: ^/token/refresh$, roles: PUBLIC_ACCESS }
        # ...
# ...

I have removed the path configuration in config/routes/gesdinet_jwt_refresh_token.yaml created by Symfony Recipes, with this configuration it will not work again. I recommend you to remove this file because it isn't helpful anymore. Then, my configuration is like this :

# gesdinet_jwt_refresh_token:
    # path: /token/refresh
# ...

Now, it works so good ! So. That solution is just temporary, I guest they will fix it. I wish I helped you. 👍🏾

Servialux commented 1 year ago

Work well with configuration :

    refresh_jwt:
        refresh_jwt:
            check_path: /api/token/refresh
        provider:  app_user_provider

@youassi Thanks a lot work for me

Inscure commented 7 months ago

Hello, when will this be officially fixed?

SergeMezui16 commented 7 months ago

Hello, when will this be officially fixed? I don't if they really consider that it has been to be fixed!

Jayfrown commented 7 months ago

Hello, when will this be officially fixed?

This has, in fact, been officially fixed @Inscure

https://github.com/markitosgv/JWTRefreshTokenBundle/pull/303 has been merged on April 7, 2022 and any release >= v1.1.1 contains the fix.

Make sure that:

  1. Your routes.yaml contains something like this:
routes.yaml ```yaml api_token_refresh: path: /api/token/refresh ```
  1. Your security.yaml contains something like this:
security.yaml ```yaml security: firewalls: api: pattern: ^/api stateless: true entry_point: jwt jwt: ~ refresh_jwt: check_path: api_token_refresh access_control: - { path: ^/api/token, roles: PUBLIC_ACCESS } ```

Requests to your defined route will hit the RefreshTokenAuthenticator:

❯ curl -k -X GET https://localhost/api/token/refresh
{"code":401,"message":"Missing JWT Refresh Token"}

❯ curl -k -X POST https://localhost/api/token/refresh
{"code":401,"message":"Missing JWT Refresh Token"}
bobas01 commented 7 months ago

hi everywone, i try all youre solution for to have the same result when i want to migrate : Unrecognized options "secret, lifetime, path" under "security.firewalls.remember_me". Available options are "access_denied_handler", "access_denied_url", "access_token", "context", "custom_authenticators
", "entry_point", "form_login", "form_login_ldap", "host", "http_basic", "http_basic_ldap", "json_login", "json_login_ldap", "jwt", "lazy", "login_link", "login_throttling", "logout", "methods", "pattern
", "provider", "refresh_jwt", "remember_me", "remote_user", "request_matcher", "required_badges", "security", "stateless", "switch_user", "user_checker", "x509".

If someone have an idea thks

SergeMezui16 commented 7 months ago

hi everywone, i try all youre solution for to have the same result when i want to migrate : Unrecognized options "secret, lifetime, path" under "security.firewalls.remember_me". Available options are "access_denied_handler", "access_denied_url", "access_token", "context", "custom_authenticators ", "entry_point", "form_login", "form_login_ldap", "host", "http_basic", "http_basic_ldap", "json_login", "json_login_ldap", "jwt", "lazy", "login_link", "login_throttling", "logout", "methods", "pattern ", "provider", "refresh_jwt", "remember_me", "remote_user", "request_matcher", "required_badges", "security", "stateless", "switch_user", "user_checker", "x509".

If someone have an idea thks

Can you share your entire configuraion ? security.yaml and route.yaml ?

bobas01 commented 7 months ago

`security: enable_authenticator_manager: true

password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: "auto"

providers:

app_user_provider:
  entity:
    class: App\Entity\User
    property: email
jwt:
  lexik_jwt:
    class: App\Entity\User

firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false login: pattern: ^/api/login stateless: true json_login: check_path: /api/login success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure provider: app_user_provider

api:
  pattern: ^/api
  stateless: true
  provider: jwt
  jwt: ~

remember_me:
  secret: "%kernel.secret%"
  lifetime: 604800
  path:
    /

access_control:

controllers: resource: path: ../src/Controller/ namespace: App\Controller type: attribute api_login_check: path: /api/login_check api_refresh_token: path: /api/token/refresh

bobas01 commented 7 months ago

@SergeMezui16 thks dude it's working. I put under api but i don't know if is the good one. But i don't have error

SergeMezui16 commented 7 months ago

@bobas01 You shouldn't put your remember_me configuration at the same level with firewalls but under a specific firewall as said in symfony doc.

Your security.yaml configuration should look like this:

# security.yaml
security:
    # ....

    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
        jwt:
            lexik_jwt:
            class: App\Entity\User

    firewalls:
    #...
        login:
            pattern: ^/api/login
            stateless: true
            provider: app_user_provider
            json_login:
                check_path: /api/login
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure

         api:
             pattern: ^/api
             stateless: true
             provider: jwt
             jwt: ~
             # Delete all cookies on logout
             logout:
                 path: api_token_invalidate
                 delete_cookies:
                     refresh_token:
                         path: /
                         samesite: none
                     token:
                         path: /
                         samesite: none

        # if you use jwt refresh token
        api_token_refresh:
            pattern: ^/token/refresh$
            stateless: true
            provider: app_user_provider
            user_checker: App\Security\UserChecker
            refresh_jwt:
                 check_path: api_refresh_token

        main:
            lazy: true
            provider: app_user_provider
            stateless: true
            remember_me:
                secret: "%kernel.secret%"
                lifetime: 604800

# ...

And your route.yaml :

# route.yaml
# ...
api_refresh_token:
    path: /token/refresh
    methods: [POST]
# ...
bobas01 commented 7 months ago

@SergeMezui16 i try your code and i come back with th same error In ArrayNode.php line 327:

Unrecognized option "api_token_refresh" under "security.firewalls.api". Available options are "access_denied_handler", "access_denied_url", "access_token", "context", "custom_authenticators", "entry_poin
t", "form_login", "form_login_ldap", "host", "http_basic", "http_basic_ldap", "json_login", "json_login_ldap", "jwt", "lazy", "login_link", "login_throttling", "logout", "methods", "pattern", "provider",
"refresh_jwt", "remember_me", "remote_user", "request_matcher", "required_badges", "security", "stateless", "switch_user", "user_checker", "x509".

SergeMezui16 commented 7 months ago

@bobas01 My bad. I put main and api_token_refresh firewalls under api instead of firewalls 👎🏾 ! You can try this again i change it ;)!

ozahorulia commented 5 months ago

For those who is facing a similar problem, check if any other firewall doesn't override your refresh firewall. E.g. I had this config before updating to 5.4 style:

        api_auth:
            pattern: ^(\/[a-z]{2})?/api/token/(refresh|verify)
            stateless: false
        api_refresh:
            pattern: ^(\/[a-z]{2})?/api/token/refresh
            stateless: false
            provider: database_users
            refresh_jwt:
                check_path: gesdinet_jwt_refresh_token

I was struggling for hours until I noticed that api_auth firewall matched the refresh path, thus api_refresh firewall didn't work at all. So silly mistake but I suppose someone may doing same mistake without realizing it :)

mohammad-salehe-alizadegan commented 2 days ago

JWT Refresh Token Implementation and User Provider Fix

@Bohilc This document outlines the solution to a common issue with JWT refresh tokens and user provider configuration in Symfony applications, particularly after Symfony 5.

The previous configuration often relied on deprecated settings and lacked a robust user provider implementation. This guide provides a corrected and improved approach.

Problem

Previous configurations might have used deprecated. bundle settings or had incomplete user provider implementations, leading to issues with refreshing JWT tokens.

Solution

  1. Security Configuration (config/packages/security.yaml)

    
    security:
       providers:
           app_user_provider:  # Define your user provider
               entity:
                   class: App\Entity\User
                   property: username  # Or email, adjust as needed
    
       firewalls:
           main:
               # ... other firewall settings
               provider: app_user_provider # Reference the provider
               # ... your authentication mechanisms (e.g., login_form, api_token)
    
       access_control:
           - { path: ^/api/token/refresh, roles: PUBLIC_ACCESS } # Public access to refresh endpoint

Routing Configuration (config/routes.yaml)

refresh_jwt:
    path: /api/token/refresh
    controller: App\Controller\RefreshTokenController::refresh # Replace with your actual controller and method

User Provider (src/Security/UserProvider.php)
<?php

namespace App\Security;

use App\Entity\User;
use App\Repository\UserRepository;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;

class UserProvider implements UserProviderInterface
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    /**
     * Symfony calls this method if you use features like switch_user
     * or remember_me.
     *
     * If you're not using these features, you do not need to implement
     * this method.
     *
     * @throws UnsupportedUserException if the account is not supported
     */
    public function refreshUser(UserInterface $user): UserInterface
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class));
        }

        return $this->loadUserByIdentifier($user->getUserIdentifier());
    }

    /**
     * Whether this provider supports the given user class.
     *
     * @param string $class
     * @return bool
     */
    public function supportsClass(string $class): bool
    {
        return User::class === $class || is_subclass_of($class, User::class);
    }

    /**
     * @param string $identifier
     * @return UserInterface
     */
    public function loadUserByIdentifier(string $identifier): UserInterface
    {
        if (is_numeric($identifier)){
            $user = $this->userRepository->find($identifier); // Use the correct field (username, email, etc.)
        }
        else{
            $user = $this->userRepository->findOneBy(['username'=>$identifier]); // Use the correct field (username, email, etc.)
        }

        if (null === $user) {
            throw new UserNotFoundException(sprintf('User "%s" not found.', $identifier));
        }

        return $user;
    }

}
mohammad-salehe-alizadegan commented 2 days ago

@Bohilc if you had any other question you can ask here.