Description
Symfony version(s) affected
6.4.5
Description
Hi everyone,
I'm currently developping a new Symfony 6.4.5 app which needs a custom Authenticator and a custom User Provider.
Basically the reverse proxy behind the application adds a specif http header with a token which should be used by the app to fetch user data through an dedicated external API.
Here is the problem i noticed:
I'm working on mode stateless = false; so i want to authenticate the user and fetch the api only for the first request. The other requests are supposed to retrieve the user from the session but it doesn't behave like that
How to reproduce
Here is the code of my Provider and my custom Authenticator (for my tests, i am very close to the default symfony documentation example):
use App\Security\User\CustomUserProvider;
class CustomUserProvider im
86C0
plements UserProviderInterface
{
/**
* 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 UserNotFoundException if the user is not found
*/
public function loadUserByIdentifier(string $identifier): UserInterface
{
dump("load user by identifier");
$user = new User(123456789,"email",['ROLE_ADMIN','ROLE_USER']);
return $user;
// Load a User object from your data source or throw UserNotFoundException.
// The $identifier argument is whatever value is being returned by the
// getUserIdentifier() method in your User class.
throw new \Exception('TODO: fill in loadUserByIdentifier() inside '.__FILE__);
}
/**
* Refreshes the user after being reloaded from the session.
*
* When a user is logged in, at the beginning of each request, the
* User object is loaded from the session and then this method is
* called. Your job is to make sure the user's data is still fresh by,
* for example, re-querying for fresh User data.
*
* If your firewall is "stateless: true" (for a pure API), this
* method is not called.
*/
public function refreshUser(UserInterface $user): UserInterface
{
dump("refresh User",$user);
if (!$user instanceof UserInterface) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', get_class($user)));
}
return $user;
// Return a User object after making sure its data is "fresh".
// Or throw a UserNotFoundException if the user no longer exists.
throw new \Exception('TODO: fill in refreshUser() inside '.__FILE__);
}
/**
* Tells Symfony to use this provider for this User class.
*/
public function supportsClass(string $class): bool
{
return User::class === $class || is_subclass_of($class, User::class);
}
/**
* Upgrades the hashed password of a user, typically for using a better hash algorithm.
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
// TODO: when hashed passwords are in use, this method should:
// 1. persist the new password in the user storage
// 2. update the $user object with $user->setPassword($newHashedPassword);
}
}
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class CustomAuthenticator extends AbstractAuthenticator
{
/**
* Called on every request to decide if this authenticator should be
* used for the request. Returning `false` will cause this authenticator
* to be skipped.
*/
public function supports(Request $request): ?bool
{
return true;
}
public function authenticate(Request $request): Passport
{
dump("authenticate");
// implement your own logic to get the user identifier from `$apiToken`
// e.g. by looking up a user in the database using its API key
$userIdentifier = "fake id";
return new SelfValidatingPassport(new UserBadge($userIdentifier));
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// on success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
// you may want to customize or obfuscate the message first
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
// or to translate this message
// $this->translator->trans($exception->getMessageKey(), $exception->getMessageData())
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}
(security.yaml)
security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# users_in_memory: { memory: null }
rush_user_provider:
id: App\Security\Provider\CustomUserProvider
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
stateless: false
provider: custom_user_provider
custom_authenticators:
- App\Security\CustomAuthenticator
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
switch_user: false
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
When i try the two scenarios:
1 (first request for a user)
--> enter in "support" function (twice)
--> enter in authenticate function
--> enter in 'loadUserByIdentifier' function a create the user
This scenario works as expected: The user has no session: he is authenticated and the user is loaded from the API and the stored in session.
2 (others requests)
--> enter in 'refreshUser' function( and the user is correctly retrieved from the session)
--> enter in "support" function (twice)
--> enter in authenticate function (again ?)
--> enter in 'loadUserByIdentifier' function and re-create the user
I was expceting to enter only in 'refreshUser' function when the session exists
I'm not using the stateless: true option in this case.
Is this the normal behavior ?
Possible Solution
No response
Additional Context
I found an old similar issue (which was patched) with the 'REMOTE_USER' provider.
#43648