feat: harden the myep OAuth2 login

This commit is contained in:
fromme committed 2026-09-23 15:18:43 +02:00
1 parent 4517b84023
commit 91419294b8
25 files changed
+1013 -35

No files matched your search

+73
View File
@@ -18,6 +18,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\Table(name: '`user`')]
#[ORM\UniqueConstraint(name: 'uniq_user_email', columns: ['email'])]
#[ORM\UniqueConstraint(name: 'uniq_user_sub', columns: ['sub'])]
class User implements UserInterface
{
#[ORM\Id]
@@ -25,6 +26,20 @@ class User implements UserInterface
#[ORM\Column]
private ?int $id = null;
/**
* myep's account identifier: opaque, stable, and the only claim that identifies an account.
* Nullable because rows provisioned before it was stored have none -- those are matched on
* their email once more and backfilled on that login, so the column empties out by itself.
*/
#[ORM\Column(length: 64, nullable: true)]
private ?string $sub = null;
/**
* The address this session authenticated with, normalised to lower case. Distinct per myep
* account and kept up to date from the claim, but a display and contact value rather than
* the key -- one person can hold several myep accounts, each with its own address and its
* own roles.
*/
#[ORM\Column(length: 180)]
#[Assert\NotBlank]
#[Assert\Email]
@@ -42,12 +57,34 @@ class User implements UserInterface
#[ORM\Column]
private array $roles = [];
/**
* When the account was locked out here, or null while it may sign in.
*
* Local and deliberately not derived from anything myep says: it is the only lever this
* application has over its own access, and a login must not be able to clear it.
*/
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $disabledAt = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $disabledReason = null;
public function __construct(string $email, string $name)
{
$this->email = $email;
$this->name = $name;
}
public function getSub(): ?string
{
return $this->sub;
}
public function setSub(?string $sub): void
{
$this->sub = $sub;
}
public function getId(): ?int
{
return $this->id;
@@ -58,6 +95,11 @@ class User implements UserInterface
return $this->email;
}
public function setEmail(string $email): void
{
$this->email = $email;
}
public function getName(): string
{
return $this->name;
@@ -89,6 +131,37 @@ class User implements UserInterface
$this->roles = $roles;
}
public function isDisabled(): bool
{
return null !== $this->disabledAt;
}
public function getDisabledAt(): ?\DateTimeImmutable
{
return $this->disabledAt;
}
public function getDisabledReason(): ?string
{
return $this->disabledReason;
}
/**
* Re-locking an already locked account leaves the original timestamp alone: when the lockout
* started is the part worth keeping.
*/
public function disable(?string $reason = null): void
{
$this->disabledAt ??= new \DateTimeImmutable();
$this->disabledReason = $reason;
}
public function enable(): void
{
$this->disabledAt = null;
$this->disabledReason = null;
}
public function eraseCredentials(): void
{
}