0

I use jwt token for auth and I had to change user_identity_field to email. And after that when I try call /api/token/refresh I have 401 status code. Because for refresh token entity in username property saved username data from user

my config

lexik_jwt_authentication:
private_key_path: '%jwt_private_key_path%'
public_key_path:  '%jwt_public_key_path%'
pass_phrase:      '%jwt_key_pass_phrase%'
token_ttl:        '%jwt_token_ttl%'
user_identity_field: email

gesdinet_jwt_refresh_token:
ttl: '%jwt_refresh_token_ttl%'
ttl_update: true
user_provider: security.user.provider.concrete.chain_provider

and my security

security:
encoders:
    AppBundle\Entity\User:
        algorithm: bcrypt

    AppBundle\Entity\Admin:
        algorithm: bcrypt

providers:
    chain_provider:
        chain:
            providers: [admins, entity_provider]

    admins:
        entity:
            class: AppBundle:Admin
            property: email

    entity_provider:
        entity:
            class: AppBundle:User
            property: email

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

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

    api_admin:
        pattern:   ^/api/admin
        stateless: true
        anonymous: false
        provider: chain_provider
        guard:
            authenticators:
                - app.jwt_token_authenticator

    login:
        pattern:  ^/api/login
        stateless: true
        anonymous: true
        form_login:
            check_path: /api/login_check
            require_previous_session: false
            username_parameter: _email
            password_parameter: _password
            success_handler: custom
            failure_handler: lexik_jwt_authentication.handler.authentication_failure

now /api/token/refresh I have response

{
  "code": 401,
  "message": "Bad credentials"
}

because \Gesdinet\JWTRefreshTokenBundle\Entity\RefreshToken have username data from user, but in my config for lexik_jwt_authentication I changed it

user_identity_field: email

How to apply user_identity_field: email to refresh token ?

shuba.ivan
  • 3,824
  • 8
  • 49
  • 121

1 Answers1

0

First of all please read this article: http://symfony.com/doc/3.3/bundles/override.html#services-configuration and this one: http://symfony.com/doc/3.3/service_container/compiler_passes.html

And here is what I did for the almost the same issue (I needed user ID instead of username)

// config.yml
lexik_jwt_authentication:
    private_key_path: '%jwt_private_key_path%'
    public_key_path:  '%jwt_public_key_path%'
    pass_phrase:      '%jwt_key_pass_phrase%'
    token_ttl:        '%jwt_token_ttl%'
    user_identity_field: 'id'

    # token extraction settings
    token_extractors:
        authorization_header:      # look for a token as Authorization Header
            enabled: true
            prefix:  ~
            name: 'Authorization'

gesdinet_jwt_refresh_token:
    ttl: '%jwt_refresh_token_ttl%'
    ttl_update: true
    firewall: 'api_secured'
    user_provider: 'security.db_user_provider' # NOTE THIS

Firewall settings:

security:
    # http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
    providers:
        fos_userbundle:
            id: fos_user.user_provider.username_email
        db_provider:
            id: security.db_user_provider
    firewalls:
        refresh:
            pattern: ^/api/v1/token/refresh
            stateless: true
            anonymous: true
        api_login:
            pattern: ^/api/v1/user/login
            stateless: true
            anonymous: true
            provider: db_provider
            json_login:
                check_path: /api/v1/user/login
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
                require_previous_session: false
        api_secured:
            pattern: <removed>
            stateless: true
            anonymous: false
            provider: db_provider
            guard:
                authenticators:
                    - lexik_jwt_authentication.jwt_token_authenticator

Services definition:

// Application/UserBundle/Resources/config/services.yml
security.db_user_provider:
    class: Application\UserBundle\Util\Security\DB\UserProvider
    calls:
        - ['setDoctrine', ['@doctrine']]

security.jwtrefreshtoken.send_token:
    class: Application\UserBundle\EventListener\OverrideAttachRefreshTokenOnSuccessListener
    arguments: [ "@gesdinet.jwtrefreshtoken.refresh_token_manager", "%gesdinet_jwt_refresh_token.ttl%", "@validator", "@request_stack" ]

User provider class:

// Application/UserBundle/Util/Security/DB/UserProvider.php

class UserProvider implements UserProviderInterface
{
    /**
     * @var AbstractManagerRegistry
     */
    protected $doctrine;

    /**
     * @param AbstractManagerRegistry $doctrine
     *
     * @return $this
     */
    final public function setDoctrine(AbstractManagerRegistry $doctrine)
    {
        $this->doctrine = $doctrine;

        return $this;
    }

    /**
     * Loads the user for the given username.
     *
     * This method must throw UsernameNotFoundException if the user is not
     * found.
     *
     * @param string $username The username
     *
     * @return UserInterface
     *
     * @throws UsernameNotFoundException if the user is not found
     */
    public function loadUserByUsername($username)
    {
        // I need these 3 options because I use FOSUserBundle
        if (is_numeric($username)) {
            $user = $this->doctrine->getManager()->getRepository(User::class)->find($username);
        } elseif (filter_var($username, FILTER_VALIDATE_EMAIL)) {
            $user = $this->doctrine->getManager()->getRepository(User::class)->findOneBy(['email' => $username]);
        } else {
            $user = $this->doctrine->getManager()->getRepository(User::class)->findOneBy(['username' => $username]);
        }

        if (!$user) {
            throw new UsernameNotFoundException();
        }

        return $user;
    }

    /**
     * Refreshes the user.
     *
     * It is up to the implementation to decide if the user data should be
     * totally reloaded (e.g. from the database), or if the UserInterface
     * object can just be merged into some internal array of users / identity
     * map.
     *
     * @param UserInterface $user
     *
     * @return UserInterface
     *
     * @throws UnsupportedUserException if the user is not supported
     */
    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof User) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

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

Here is the main part:

// Application/UserBundle/EventListener/OverrideAttachRefreshTokenOnSuccessListener.php

namespace Application\UserBundle\EventListener;

use Gesdinet\JWTRefreshTokenBundle\EventListener\AttachRefreshTokenOnSuccessListener;
use Gesdinet\JWTRefreshTokenBundle\Request\RequestRefreshToken;
use Lexik\Bundle\JWTAuthenticationBundle\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\UserInterface;

class OverrideAttachRefreshTokenOnSuccessListener extends AttachRefreshTokenOnSuccessListener
{
    /**
     * @param AuthenticationSuccessEvent $event
     */
    public function attachRefreshToken(AuthenticationSuccessEvent $event)
    {
        $data = $event->getData();
        $user = $event->getUser();
        $request = $this->requestStack->getCurrentRequest();

        if (!$user instanceof UserInterface) {
            return;
        }

        $refreshTokenString = RequestRefreshToken::getRefreshToken($request);

        if ($refreshTokenString) {
            $data['refresh_token'] = $refreshTokenString;
        } else {
            $datetime = new \DateTime();
            $datetime->modify('+'.$this->ttl.' seconds');

            $refreshToken = $this->refreshTokenManager->create();
            $refreshToken->setUsername($user->getId()); // Change this to $user->getEmail() for your purpose
            $refreshToken->setRefreshToken();
            $refreshToken->setValid($datetime);

            $valid = false;
            while (false === $valid) {
                $valid = true;
                $errors = $this->validator->validate($refreshToken);
                if ($errors->count() > 0) {
                    foreach ($errors as $error) {
                        if ('refreshToken' === $error->getPropertyPath()) {
                            $valid = false;
                            $refreshToken->setRefreshToken();
                        }
                    }
                }
            }

            $this->refreshTokenManager->save($refreshToken);
            $data['refresh_token'] = $refreshToken->getRefreshToken();
        }

        $event->setData($data);
    }
}

Create new compiler:

// Application/UserBundle/DependencyInjection/Compiler/OverrideAttachRefreshTokenOnSuccessListenerCompiler.php

class OverrideAttachRefreshTokenOnSuccessListenerCompiler implements CompilerPassInterface
{
    /**
     * @param ContainerBuilder $container
     */
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition('gesdinet.jwtrefreshtoken.send_token');
        $definition->setClass(OverrideAttachRefreshTokenOnSuccessListener::class);
    }
}

And last thing - register your compiler:

class ApplicationUserBundle extends Bundle
{
    /**
     * @param ContainerBuilder $container
     */
    public function build(ContainerBuilder $container)
    {
        parent::build($container);

        $container
            ->addCompilerPass(new OverrideAttachRefreshTokenOnSuccessListenerCompiler());
    }
}