feat: identify accounts by a uuid in the oauth2 sub claim

This commit is contained in:
fromme committed 2026-09-23 15:17:09 +02:00
1 parent cc3d32fcbb
commit bad916cfb7
6 files changed
+153 -7

No files matched your search

+5 -1
View File
@@ -58,7 +58,11 @@ class UserinfoController extends AbstractController
// addresses on a person's record as a login and answers all of them with the same
// ids and the same first contact address, so only the local account tells the staff
// account and the private one apart — and they hold different roles.
$data->subject = (string) $user->getId();
//
// The uuid, not the primary key: consumers store this value and match their own
// accounts on it, so it must not disclose how many accounts exist or let one
// account's identifier be guessed from another's.
$data->subject = $user->getUuid();
$data->loginEmail = $user->getEmail();
// Patch current user's roles. The implicit ROLE_USER says nothing about the
+23
View File
@@ -6,6 +6,7 @@ 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 Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface, PasswordAuthenticatedUserInterface
@@ -15,6 +16,22 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column(type: 'integer')]
private ?int $id = null;
/**
* The account's external identifier, exported as the OAuth2 `sub` claim.
*
* Separate from the primary key on purpose: `sub` is handed to other applications, which
* store it and match their own accounts on it, so it has to be stable, unique and free of
* anything the row's position in the table implies. The auto-increment id satisfied the
* first two and neither of the last: it disclosed how many accounts exist and made one
* account's identifier a guess away from the next.
*
* v4 rather than the framework's default v7 (see config/packages/uid.yaml): v7 embeds its
* creation timestamp and sorts, which is useful for a key and is exactly the leak this
* column exists to avoid. Matches Groups\AccommodationBooking::$uuid in shape and version.
*/
#[ORM\Column(type: 'string', length: 36, unique: true)]
private string $uuid;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private ?string $email;
@@ -54,6 +71,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
public function __construct(string $email)
{
$this->email = $email;
$this->uuid = Uuid::v4()->toRfc4122();
}
public function getId(): ?int
@@ -61,6 +79,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this->id;
}
public function getUuid(): string
{
return $this->uuid;
}
public function getEmail(): ?string
{
return $this->email;