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