Search code examples
phpsymfonysymfony4symfony-securitysymfony5

Prevent user from logging in if his status is inactive


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;
    }
}

Solution

  • 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.