4

I am trying to block user from logging in his status is inactive. I am using API-Platform with LexikJWT bundle.

I have tried to make a JWTAuthentication guard by extending JWTTokenAuthenticator->checkCredentials but the problem is that this works after user already logged in.

What I want to achieve is to return user a message that he needs to activate his account first, or any other message, preferably any custom message on any custom condition.

My security YAML looks like this:

security:
    encoders:
        App\Entity\User:
            algorithm: bcrypt
    providers:
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
    firewalls:
        dev:
            pattern: ^/_(profiler|wdt)
            security: false
        api:
            pattern: ^/api/
            stateless: true
            anonymous: true
            provider: app_user_provider
            json_login:
                check_path: /api/authentication_token
                username_path: email
                password_path: password
                success_handler: lexik_jwt_authentication.handler.authentication_success
                failure_handler: lexik_jwt_authentication.handler.authentication_failure
            guard:
                authenticators:
                    - app.jwt_token_authenticator
        main:
            anonymous: true
    access_control:
        - { path: ^/api/authentication_token,   roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/graphql,                roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/public-api,                 roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/api/,                       roles: [ROLE_MANAGER, ROLE_LEADER] }
        - { path: ^/,                           roles: IS_AUTHENTICATED_ANONYMOUSLY }

Services:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones
    gedmo.listener.softdeleteable:
        class: Gedmo\SoftDeleteable\SoftDeleteableListener
        tags:
            - { name: doctrine.event_subscriber, connection: default }
        calls:
            - [ setAnnotationReader, [ '@annotation_reader' ] ]

    acme_api.event.authentication_success_listener:
        class: App\EventListener\AuthenticationSuccessListener
        tags:
            - { name: kernel.event_listener, event: lexik_jwt_authentication.on_authentication_success, method: onAuthenticationSuccessResponse }

    app.jwt_token_authenticator:
        autowire: false
        autoconfigure: false
        class: App\Security\Guard\JWTTokenAuthenticator
        parent: lexik_jwt_authentication.security.guard.jwt_token_authenticator

    'App\Serializer\ApiNormalizer':
        decorates: 'api_platform.serializer.normalizer.item'
        arguments: ['@App\Serializer\ApiNormalizer.inner', '@doctrine.orm.entity_manager']

    'App\Serializer\HydraApiNormalizer':
        decorates: 'api_platform.jsonld.normalizer.item'
        arguments: ['@App\Serializer\ApiNormalizer.inner', '@doctrine.orm.entity_manager']

    'App\Voter\ModifyUserVoter':
        public: false
        tags:
            - { name: security.voter }

Authenticator guard

class JWTTokenAuthenticator extends BaseAuthenticator
{
    /**
     * {@inheritdoc}
     */
    public function checkCredentials($credentials, UserInterface $user)
    {
        if (!$user->getRoles() || !in_array($user->getRoles()[0], ['ROLE_MANAGER', 'ROLE_LEADER'])) {
            throw new UnauthorizedHttpException(rand(10000, 99999), 'Unauthorized');
        }

        if (!$user->getStatus() != "active") {
            throw new UnauthorizedHttpException(rand(10000, 99999), 'Unauthorized');
        }

        return true;
    }
}
yivi
  • 42,438
  • 18
  • 116
  • 138
Erik Kubica
  • 1,180
  • 3
  • 15
  • 39

2 Answers2

6

You need to create an implementation of UserCheckerInterface. (Docs)

For example, look at this:

use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;

class EasyUserChecker implements UserCheckerInterface
{
    public function checkPreAuth(UserInterface $user): void
    {
        // my checker only cares for our managed user classes, we return with no action 
        if (!$user instanceof AppAdmin && !$user instanceof AppUser) {
            return;
        }

        // our user entities can be deleted or disabled. If the user is neither, we return with no action
        if (!$user->isDeleted() && !empty($user->isEnabled())) {
            return;
        }

        // if we got here, we throw an exception
        throw new DisabledException('User account is disabled.');
    }

    // I'm not using the post authorization check, but needs to have an implementation to satisfy the interface.
    public function checkPostAuth(UserInterface $user): void
    {
    }
}

You enable the checker in your security configuration. E.g.:

security:
    firewalls:
        api:
            pattern: ^/api
            user_checker: App\Security\EasyChecker

You shouldn't write new implementations of AdvancedUserInterface nowadays. Using that as a solution is the wrong way to go.

That interface is deprecated since 4.1, and altogether removed in Symfony 5. So code that relies on that won't be upgradeable to newer Symfony versions.

yivi
  • 42,438
  • 18
  • 116
  • 138
  • Thanks I will try it out at work and I will accept your answer. Thank you also for samples and explanation I really appreciate it – Erik Kubica Feb 22 '20 at 11:32
  • thanks, this works as expected. I have replaced AdvancedUserInterface with UserInterace. One small thing, after logging in I have changed the status of the user to see how it behaves when user is logged in and suddenly he is disabled, then it works, but it does not return the error message passed to the DisabledException like it does when logging in, instead it returns some default message, i guess that something is overriding that, any idea what or where? – Erik Kubica Feb 24 '20 at 09:33
-1

I have managed to achieve what I wanted by implementing AdvancedUserInterface instead of UserInterface on the User entity and adding logic to the isEnabled() method.

yivi
  • 42,438
  • 18
  • 116
  • 138
Erik Kubica
  • 1,180
  • 3
  • 15
  • 39
  • 1
    `AdvancedUserInterface` is deprecated since 4.1, and removed on 5. Check the other answer to do it in forward-compatible way. – yivi Feb 22 '20 at 08:09