Skip to content

Commit e66facc

Browse files
committed
Add OAuth info to the User entity
1 parent 919af2d commit e66facc

4 files changed

Lines changed: 115 additions & 7 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use CodedMonkey\Dirigent\Doctrine\Entity\User;
8+
use Doctrine\DBAL\Connection;
9+
use Doctrine\DBAL\Schema\Schema;
10+
use Doctrine\Migrations\AbstractMigration;
11+
use Psr\Log\LoggerInterface;
12+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
13+
14+
final class Version20260927080101 extends AbstractMigration
15+
{
16+
public function __construct(
17+
Connection $connection,
18+
LoggerInterface $logger,
19+
private readonly UserPasswordHasherInterface $passwordHasher,
20+
) {
21+
parent::__construct($connection, $logger);
22+
}
23+
24+
public function getDescription(): string
25+
{
26+
return 'Add OAuth info to users';
27+
}
28+
29+
public function up(Schema $schema): void
30+
{
31+
$this->addSql(<<<'SQL'
32+
ALTER TABLE "user" ADD oauth_provider VARCHAR(255) DEFAULT NULL
33+
SQL);
34+
$this->addSql(<<<'SQL'
35+
ALTER TABLE "user" ADD oauth_sub VARCHAR(255) DEFAULT NULL
36+
SQL);
37+
$this->addSql(<<<'SQL'
38+
ALTER TABLE "user" ALTER password DROP NOT NULL
39+
SQL);
40+
}
41+
42+
public function down(Schema $schema): void
43+
{
44+
$this->addSql(<<<'SQL'
45+
ALTER TABLE "user" DROP oauth_provider
46+
SQL);
47+
$this->addSql(<<<'SQL'
48+
ALTER TABLE "user" DROP oauth_sub
49+
SQL);
50+
51+
// Generate a random password for each user that loses its OAuth credentials
52+
$users = $this->connection->fetchAllAssociative('SELECT id FROM "user" WHERE password IS NULL');
53+
foreach ($users as $user) {
54+
$hashedPassword = $this->passwordHasher->hashPassword(new User(), bin2hex(random_bytes(16)));
55+
$this->addSql(<<<'SQL'
56+
UPDATE "user" SET password = ? WHERE id = ?
57+
SQL, [$hashedPassword, $user['id']]);
58+
}
59+
60+
$this->addSql(<<<'SQL'
61+
ALTER TABLE "user" ALTER password SET NOT NULL
62+
SQL);
63+
}
64+
}

‎src/Doctrine/Entity/User.php‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,25 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFact
4343
#[Column(type: Types::STRING, length: 64, enumType: UserRole::class)]
4444
private UserRole $role = UserRole::User;
4545

46-
#[Column]
46+
/**
47+
* User's (hashed) password.
48+
*
49+
* The password can only be NULL when OAuth credentials are available.
50+
*/
51+
#[Column(nullable: true)]
4752
private ?string $password = null;
4853

4954
private ?string $plainPassword = null;
5055

5156
#[Column(nullable: true)]
5257
private ?string $totpSecret = null;
5358

59+
#[Column(nullable: true)]
60+
private ?string $oauthProvider = null;
61+
62+
#[Column(nullable: true)]
63+
private ?string $oauthSub = null;
64+
5465
public function __serialize(): array
5566
{
5667
// todo move plain password to DTO
@@ -151,6 +162,26 @@ public function setTotpSecret(?string $totpSecret): void
151162
$this->totpSecret = $totpSecret;
152163
}
153164

165+
public function getOauthProvider(): ?string
166+
{
167+
return $this->oauthProvider;
168+
}
169+
170+
public function setOauthProvider(?string $oauthProvider): void
171+
{
172+
$this->oauthProvider = $oauthProvider;
173+
}
174+
175+
public function getOauthSub(): ?string
176+
{
177+
return $this->oauthSub;
178+
}
179+
180+
public function setOauthSub(?string $oauthSub): void
181+
{
182+
$this->oauthSub = $oauthSub;
183+
}
184+
154185
public function getUserIdentifier(): string
155186
{
156187
return (string) $this->username;

‎src/Doctrine/EventListener/UserListener.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public function __construct(
2020

2121
public function prePersist(User $user): void
2222
{
23+
if (null !== $user->getOauthProvider() && null !== $user->getOauthSub()) {
24+
// Skip password hashing for users authenticating through OAuth
25+
return;
26+
}
27+
2328
if (null === $user->getPlainPassword()) {
2429
throw new \LogicException('A new user can\'t be created without a password.');
2530
}

‎src/Doctrine/MigrationFactory.php‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,32 @@
77
use CodedMonkey\Dirigent\Encryption\Encryption;
88
use Doctrine\DBAL\Connection;
99
use Doctrine\Migrations\AbstractMigration;
10-
use Doctrine\Migrations\Version\MigrationFactory as BaseMigrationFactory;
10+
use Doctrine\Migrations\Version\MigrationFactory as MigrationFactoryInterface;
11+
use DoctrineMigrations\Version20250311205816;
12+
use DoctrineMigrations\Version20260927080101;
1113
use Psr\Log\LoggerInterface;
14+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
1215

13-
readonly class MigrationFactory implements BaseMigrationFactory
16+
readonly class MigrationFactory implements MigrationFactoryInterface
1417
{
1518
public function __construct(
1619
private Connection $connection,
1720
private LoggerInterface $logger,
1821
private Encryption $encryptionUtility,
22+
private UserPasswordHasherInterface $passwordHasher,
1923
) {
2024
}
2125

2226
public function createVersion(string $migrationClassName): AbstractMigration
2327
{
24-
if (str_contains($migrationClassName, '20250311205816')) {
25-
return new $migrationClassName($this->connection, $this->logger, $this->encryptionUtility);
26-
}
28+
$additionalParameters = match ($migrationClassName) {
29+
Version20250311205816::class => [$this->encryptionUtility],
30+
Version20260927080101::class => [$this->passwordHasher],
31+
default => [],
32+
};
2733

28-
return new $migrationClassName($this->connection, $this->logger);
34+
$parameters = [$this->connection, $this->logger, ...$additionalParameters];
35+
36+
return new $migrationClassName(...$parameters);
2937
}
3038
}

0 commit comments

Comments
 (0)