Hi, I'm trying to implement SSO in a new setup of Symfony. SAML seems to be working and I can see the correct attributes being passed over from the IdP to Symfony and your SAML code. However, the login process fails with this error:
You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.
I've tracked it down to an empty User record being created because the loadUserByIdentifier() method is being called from your SamlUserProvider class. It is not calling my loadUserByIdentifier() method in my UserRepository class.
I am not (currently) trying to do JIT provisioning, this is testing with an existing user already in the SP database. I am using a UUID in the saml attributes as the user identifier, and my loadUserByIdentifier() is set up to load the user by that value.
I am able to track the code and data down to the createPassport() method in SamlAuthenticator, and the new UserBadge() call that appears to handle fetching the user (see the line "/* here */" in the code snippet below). In the new UserBadge() code I can see that it is properly getting the UUID from the saml attributes, but the Provider Class (get_class($this->userProvider)) is SamlUserProvider, so SamlUserProvider->loadUserByIdentifier() is being called in new UserBadge(), not UserRepository->loadUserByIdentifier().
vendor/nbgrp/onelogin-saml-bundle/src/Security/Http/Authenticator/SamlAuthenticator.php
error_log('Here-1');
$userBadge = new UserBadge(
$this->extractIdentifier($oneLoginAuth, $attributes),
function (string $identifier) use ($deferredEventBadge, $attributes) {
error_log('Here-2');
try {
try {
error_log('Loading user using identifier: ' . $identifier);
error_log('Provider Class: ' . get_class($this->userProvider));
/* here */ $user = $this->userProvider->loadUserByIdentifier($identifier);
error_log('Got user: ' . print_r($user, true));
if ($user instanceof SamlUserInterface) {
$user->setSamlAttributes($attributes);
$deferredEventBadge->setEvent(new UserModifiedEvent($user));
}
} catch (UserNotFoundException $exception) {
if (!$this->userFactory instanceof SamlUserFactoryInterface) {
throw $exception;
}
$user = $this->userFactory->createUser($identifier, $attributes);
$deferredEventBadge->setEvent(new UserCreatedEvent($user));
}
} catch (\Throwable $exception) {
if ($exception instanceof UserNotFoundException) {
throw $exception;
}
throw new AuthenticationException('The authentication failed.', 0, $exception);
}
return $user;
},
);
error_log output with the above debug code:
[PHP ] [Fri Nov 7 17:36:16 2025] Here-1
[PHP ] [Fri Nov 7 17:36:16 2025] Here-2
[PHP ] [Fri Nov 7 17:36:16 2025] Loading user using identifier: 3b73c064-0676-4f92-9ba5-e3343ab8d669
[PHP ] [Fri Nov 7 17:36:16 2025] Provider Class: Nbgrp\OneloginSamlBundle\Security\User\SamlUserProvider
[PHP ] [Fri Nov 7 17:36:16 2025] Got user: App\Entity\User Object
[PHP ] (
[PHP ] [id:App\Entity\User:private] =>
[PHP ] [uuid:App\Entity\User:private] =>
[PHP ] [username:App\Entity\User:private] =>
[PHP ] [roles:App\Entity\User:private] => Array
[PHP ] (
[PHP ] )
[PHP ]
[PHP ] [FirstName:App\Entity\User:private] =>
[PHP ] [LastName:App\Entity\User:private] =>
[PHP ] [Email:App\Entity\User:private] =>
[PHP ] [password:App\Entity\User:private] =>
[PHP ] )
[PHP ]
[
My UserRepository class was created using the 'symfony console make:user' tool (I believe that created the UserRepository class as well as the User Entity class...). I had extended that class to add the UserLoaderInterface interface and added my loadUserByIdentifier() method per these instructions on setting up Symfony: https://symfony.com/doc/current/security/user_providers.html#using-a-custom-query-to-load-the-user. With that setup, I get the above error.
I've tried extending my UserRepository to also implement UserProviderInterface - which is what your code and setup instructions appear to be expecting, but that doesn't seem to help - SamlUserProvider->loadUserByIdentifier() still gets called. With UserProviderInterface added, the error does go away and I can get past the login page, but my user record hasn't been loaded properly into the session (it gets the uuid assigned to it in User->setSamlAttributes() but it doesn't get the data from the database so it is missing username, email, first/last name, etc.).
I've added the SamlUserInterface to my User entity class as well (per your setup instructions).
Sorry if this is a bit all over the place... I've been coding for a long time, but very new to Symfony. I'm using Symfony 7.3.5 with php version 8.4.14.
I'm not sure if I've missed something in your setup instructions, or if this package was written for an earlier version of Symfony (one that uses UserProviderInterface instead of UserLoaderInterface)... Any help would be appreciated!
security.yaml (relevant lines):
security:
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
# used to reload user from session & other features (e.g. switch_user)
saml_provider:
## Basic provider instantiates a user with identifier and default roles
saml:
user_class: App\Entity\User
default_roles: ['ROLE_USER']
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
main:
pattern: ^/
provider: saml_provider
saml:
## Match SAML attribute 'uid' with user identifier.
## Otherwise, used \OneLogin\Saml2\Auth::getNameId() method by default.
identifier_attribute: uuid
## Use the attribute's friendlyName instead of the name.
use_attribute_friendly_name: false
check_path: saml_acs
login_path: saml_login
persist_user: false
logout:
path: saml_logout
# 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: ^/saml/(metadata|login|acs), roles: PUBLIC_ACCESS }
src/Entity/User.php (partial)
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Nbgrp\OneloginSamlBundle\Security\User\SamlUserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_UUID', fields: ['uuid'])]
class User implements UserInterface, PasswordAuthenticatedUserInterface, SamlUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180)]
private ?string $uuid = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $username = null;
...
/**
* Work with SAML data on login
*/
public function setSamlAttributes(array $attributes) : void
{
error_log('SAML Attributes: ' . print_r($attributes, true));
$this->uuid = $attributes['uuid'][0];
}
...
src/Repository/UserRepository.php (full)
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*/
#class UserRepository extends ServiceEntityRepository implements UserLoaderInterface, UserProviderInterface
class UserRepository extends ServiceEntityRepository implements UserLoaderInterface
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, User::class);
}
public function loadUserByIdentifier(string $loginIdentifier): UserInterface
{
error_log('AAA: Querying for user using: ' . $loginIdentifier);
$entityManager = $this->getEntityManager();
$u = $entityManager->createQuery(
'SELECT u
FROM App\Entity\User u
WHERE LOWER(u.uuid) = :query
OR LOWER(u.username) = :query
OR LOWER(u.Email) = :query'
)
->setParameter('query', strtolower($loginIdentifier))
->getOneOrNullResult();
error_log('AAA: got: ' . print_r($u, true));
return $u;
}
/**
* 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.
*/
/* enable with UserProviderInterface...
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Invalid user class "%s".', $user::class));
}
// Return a User object after making sure its data is "fresh".
// Or throw a UserNotFoundException if the user no longer exists.
return $this->loadUserByIdentifier($user->getUuid());
}
*/
/**
* Tells Symfony to use this provider for this User class.
*/
/* enable with UserProviderInterface...
public function supportsClass(string $class): bool
{
return User::class === $class || is_subclass_of($class, User::class);
}
*/
}
Hi, I'm trying to implement SSO in a new setup of Symfony. SAML seems to be working and I can see the correct attributes being passed over from the IdP to Symfony and your SAML code. However, the login process fails with this error:
You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.I've tracked it down to an empty User record being created because the loadUserByIdentifier() method is being called from your SamlUserProvider class. It is not calling my loadUserByIdentifier() method in my UserRepository class.
I am not (currently) trying to do JIT provisioning, this is testing with an existing user already in the SP database. I am using a UUID in the saml attributes as the user identifier, and my loadUserByIdentifier() is set up to load the user by that value.
I am able to track the code and data down to the createPassport() method in SamlAuthenticator, and the new UserBadge() call that appears to handle fetching the user (see the line "/* here */" in the code snippet below). In the new UserBadge() code I can see that it is properly getting the UUID from the saml attributes, but the Provider Class (get_class($this->userProvider)) is SamlUserProvider, so SamlUserProvider->loadUserByIdentifier() is being called in new UserBadge(), not UserRepository->loadUserByIdentifier().
vendor/nbgrp/onelogin-saml-bundle/src/Security/Http/Authenticator/SamlAuthenticator.php
error_log output with the above debug code:
My UserRepository class was created using the 'symfony console make:user' tool (I believe that created the UserRepository class as well as the User Entity class...). I had extended that class to add the UserLoaderInterface interface and added my loadUserByIdentifier() method per these instructions on setting up Symfony: https://symfony.com/doc/current/security/user_providers.html#using-a-custom-query-to-load-the-user. With that setup, I get the above error.
I've tried extending my UserRepository to also implement UserProviderInterface - which is what your code and setup instructions appear to be expecting, but that doesn't seem to help - SamlUserProvider->loadUserByIdentifier() still gets called. With UserProviderInterface added, the error does go away and I can get past the login page, but my user record hasn't been loaded properly into the session (it gets the uuid assigned to it in User->setSamlAttributes() but it doesn't get the data from the database so it is missing username, email, first/last name, etc.).
I've added the SamlUserInterface to my User entity class as well (per your setup instructions).
Sorry if this is a bit all over the place... I've been coding for a long time, but very new to Symfony. I'm using Symfony 7.3.5 with php version 8.4.14.
I'm not sure if I've missed something in your setup instructions, or if this package was written for an earlier version of Symfony (one that uses UserProviderInterface instead of UserLoaderInterface)... Any help would be appreciated!
security.yaml (relevant lines):
src/Entity/User.php (partial)
src/Repository/UserRepository.php (full)