I've been trying for days to get Symfony 3 authentication working with a custom "user provider". I've followed this tutorial in the Symfony docs: https://symfony.com/doc/current/security/custom_provider.html which shows the example of getting user data from a webservice.
I've done everything according to that page (and other pages in Symfo docs), and tried many variation of the details. The Symfony authentication never calls my WebserviceUserProvider. My WebserviceUserProvider implements loadUserByUsername($username) as required, but an echo statement in that method never executes.
Here are files created or changed. WebserviceUser:
namespace AppBundle\Security\User;
/*
* src/AppBundle/Security/User/WebserviceUser.php
*/
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
class WebserviceUser implements UserInterface, EquatableInterface, \Serializable
{
private $id = 0;
private $username;
private $password;
private $email;
private $isActive;
private $roles;
public function __construct()
{
// DEGUBBING
echo "creating new WebserviceUser";
}
// ----------------- getters
public function getId() { return $this->id; }
public function getEmail() { return $this->email; }
public function isActive() { return $this->isActive; }
// next four are required by the interface
public function getRoles() { return $this->roles; }
public function getPassword() { return $this->password; }
public function getSalt() { return null; }
public function getUsername() { return $this->username; }
// ------------------------------ setters
public function setId(int $i) { $this->id = $i; }
public function setEmail(string $s) { $this->email = $s; }
public function setIsActive(bool $b) { $this->isActive = $b; }
public function setRoles(array $a) { $this->roles = $a; }
public function setPassword(string $s) { $this->password = $s; }
public function setSalt(string $s) { $this->salt = $s; }
public function setUsername(string $s) { $this->username = $s; }
// ----------------------------- misc.
// required by interface
public function eraseCredentials()
{
}
/** @see \Serializable::serialize() */
public function serialize()
{
// copied exactly from tuto
// ...
/** @see \Serializable::unserialize() */
public function unserialize($serialized)
{
// copied exactly from tuto
// ...
public function isEqualTo(UserInterface $user)
{
// copied exactly from tuto
// ...
}
The WebserviceUserProvider:
namespace AppBundle\Security\User;
/*
* src/AppBundle/Security/User/WebserviceUserProvider.php
*/
use AppBundle\Security\User\WebserviceUser;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
class WebserviceUserProvider implements UserProviderInterface
{
public function loadUserByUsername($username)
{
// Symfo docs sample: "make a call to your webservice here
// $userData = ...
// "
echo "This is WebserviceUserProvider::loadUserByUsername()";
$user = new WebserviceUser();
$user->setUsername('testname');
$user->setPassword('testpass');
$user->setEmail('[email protected]');
$user->setIsActive(true);
return $user;
}
public function refreshUser(UserInterface $user)
{
if(!$user instanceof WebserviceUser){
throw new UnsupportedUserException(
'Class is ' . get_class($user) . ', must be WebserviceUser');
}
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class)
{
return WebserviceUser::class === $class;
}
}
My security.yml:
# app/config/security.yml
# http://symfony.com/doc/current/security.html
security:
encoders:
# BCrypt encoder
### Acme\DemoBundle\Entity\User4:
Symfony\Component\Security\Core\User\User:
algorithm: bcrypt
cost: 13
AppBundle\Security\User\WebserviceUser:
algorithm: bcrypt
cost: 13
AppBundle\Entity\UserRegister:
algorithm: bcrypt
cost: 13
providers:
#in_memory:
# memory: ~
webservice:
id: AppBundle\Security\User\WebserviceUserProvider
//hide_user_not_found: false
firewalls:
# disables authentication for assets and the profiler,
# adapt it according to your needs
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
#http_basic: ~
form_login:
login_path: login
check_path: login
anonymous: ~
# activate different ways to authenticate
access_control:
# Order matters in this list. Each path controls all under it and later
# more specific does not overrule - so go from more specific to less.
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/survey, roles: ROLE_ORG_USER }
- { path: ^/, roles: IS_AUTHENTICATED_ANONYMOUSLY }
// ...
The access_control part, and the form page all work as expected. A login attempt always gets the message Invalid credentials (as it should). Looking at the output of $authUtils->getLastAuthenticationError() in the controller gets a stack trace that does not contain any reference to "webservice".
I've tried many different tweaks of all the above, looked up many Q&A on SO and many howtos, without finding anything helpful. The above is incomplete as you can tell, but all I need is to get Symfony to use the provider and then I can handle the rest.
It's obviously working for others out there but I can't tell what they're doing differently. What am I missing?
Edit: The stack trace is like this:
error: Symfony\Component\Security\Core\Exception\BadCredentialsException: The presented password is invalid. in /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php:67 Stack trace:
#0 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php(144): session_start()
#1 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Storage/NativeSessionStorage.php(282): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->start()
#2 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Session.php(259): Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage->getBag('attributes')
#3 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpFoundation/Session/Session.php(87): Symfony\Component\HttpFoundation\Session\Session->getAttributeBag()
#4 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/ContextListener.php(83): Symfony\Component\HttpFoundation\Session\Session->get('_security_main')
#5 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall.php(69): Symfony\Component\Security\Http\Firewall\ContextListener->handle(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#6 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/EventListener/FirewallListener.php(48): Symfony\Component\Security\Http\Firewall->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#7 [internal function]: Symfony\Bundle\SecurityBundle\EventListener\FirewallListener->onKernelRequest(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher))
#8 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php(104): call_user_func(Array, Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher))
#9 [internal function]: Symfony\Component\EventDispatcher\Debug\WrappedListener->__invoke(Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher))
#10 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/EventDispatcher.php(212): call_user_func(Object(Symfony\Component\EventDispatcher\Debug\WrappedListener), Object(Symfony\Component\HttpKernel\Event\GetResponseEvent), 'kernel.request', Object(Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher))
#11 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/EventDispatcher.php(44): Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(Array, 'kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#12 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php(146): Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#13 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php(129): Symfony\Component\EventDispatcher\Debug\TraceableEventDispatcher->dispatch('kernel.request', Object(Symfony\Component\HttpKernel\Event\GetResponseEvent))
#14 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpKernel.php(68): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
#15 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Kernel.php(171): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
#16 /srv/www/site1/web/app_dev.php(28): Symfony\Component\HttpKernel\Kernel->handle(Object(Symfony\Component\HttpFoundation\Request))
#17 /srv/www/site1/vendor/symfony/symfony/src/Symfony/Bundle/WebServerBundle/Resources/router.php(42): require('/srv/www/site1...')
#18 {main}
Update:
The last (first-listed) item in the trace looked the most likely, that is vendor/symfony/symfony/src/Symfony/Component/Security/Core/Authentication/Provider/DaoAuthenticationProvider.php. And DaoAuthenticationProvider::checkAuthentication() (where l. 67 is) takes arguments UserInterface $user and UsernamePasswordToken $token, so I put dump($user); and dump($token); just before the if ... throw, and finally understood what's been happening.
As Grzegorz Gajda's reply said, the Symfony code tries each provider and goes for another one if the username doesn't match - but the thing that threw me off was that it somehow suppresses the echo statement if there's no match! So it seemed as if it wasn't hitting the service at all. But as soon as I put the dump statements in, the echos appeared and confirmed it was hitting the provider.
And once I hardcoded some known-good values, it authenticated and I was logged in. This proves it does work, albeit misleadingly, and now I just need to hook up the actual request to the data source and it will all be good.