Skip to content

Commit 0811760

Browse files
committed
Add OAuth info to the User entity
1 parent b4a0c78 commit 0811760

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 Version20260427080101 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
@@ -39,14 +39,25 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, TwoFact
3939
#[Column(type: Types::STRING, length: 64, enumType: UserRole::class)]
4040
private UserRole $role = UserRole::User;
4141

42-
#[Column]
42+
/**
43+
* User's (hashed) password.
44+
*
45+
* The password can only be NULL when OAuth credentials are available.
46+
*/
47+
#[Column(nullable: true)]
4348
private ?string $password = null;
4449

4550
private ?string $plainPassword = null;
4651

4752
#[Column(nullable: true)]
4853
private ?string $totpSecret = null;
4954

55+
#[Column(nullable: true)]
56+
private ?string $oauthProvider = null;
57+
58+
#[Column(nullable: true)]
59+
private ?string $oauthSub = null;
60+
5061
public function getId(): ?int
5162
{
5263
return $this->id;
@@ -130,6 +141,26 @@ public function setTotpSecret(?string $totpSecret): void
130141
$this->totpSecret = $totpSecret;
131142
}
132143

144+
public function getOauthProvider(): ?string
145+
{
146+
return $this->oauthProvider;
147+
}
148+
149+
public function setOauthProvider(?string $oauthProvider): void
150+
{
151+
$this->oauthProvider = $oauthProvider;
152+
}
153+
154+
public function getOauthSub(): ?string
155+
{
156+
return $this->oauthSub;
157+
}
158+
159+
public function setOauthSub(?string $oauthSub): void
160+
{
161+
$this->oauthSub = $oauthSub;
162+
}
163+
133164
public function getUserIdentifier(): string
134165
{
135166
return (string) $this->username;

src/Doctrine/EventListener/UserListener.php

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

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

src/Doctrine/MigrationFactory.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,32 @@
55
use CodedMonkey\Dirigent\Encryption\Encryption;
66
use Doctrine\DBAL\Connection;
77
use Doctrine\Migrations\AbstractMigration;
8-
use Doctrine\Migrations\Version\MigrationFactory as BaseMigrationFactory;
8+
use Doctrine\Migrations\Version\MigrationFactory as MigrationFactoryInterface;
9+
use DoctrineMigrations\Version20250311205816;
10+
use DoctrineMigrations\Version20260427080101;
911
use Psr\Log\LoggerInterface;
12+
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
1013

11-
readonly class MigrationFactory implements BaseMigrationFactory
14+
readonly class MigrationFactory implements MigrationFactoryInterface
1215
{
1316
public function __construct(
1417
private Connection $connection,
1518
private LoggerInterface $logger,
1619
private Encryption $encryptionUtility,
20+
private UserPasswordHasherInterface $passwordHasher,
1721
) {
1822
}
1923

2024
public function createVersion(string $migrationClassName): AbstractMigration
2125
{
22-
if (str_contains($migrationClassName, '20250311205816')) {
23-
return new $migrationClassName($this->connection, $this->logger, $this->encryptionUtility);
24-
}
26+
$additionalParameters = match ($migrationClassName) {
27+
Version20250311205816::class => [$this->encryptionUtility],
28+
Version20260427080101::class => [$this->passwordHasher],
29+
default => [],
30+
};
2531

26-
return new $migrationClassName($this->connection, $this->logger);
32+
$parameters = [$this->connection, $this->logger, ...$additionalParameters];
33+
34+
return new $migrationClassName(...$parameters);
2735
}
2836
}

0 commit comments

Comments
 (0)