diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index d9ff6c988c..ef73c18ad3 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -59830,41 +59830,6 @@ parameters:
 			count: 1
 			path: tests/lib/Repository/Values/User/UserTest.php
 
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testGetName\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testIsPropertySet\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testMissingProperty\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testNewClass\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testObjectProperties\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testReadOnlyProperty\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
-		-
-			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Repository\\\\Values\\\\User\\\\UserTest\\:\\:testUnsetProperty\\(\\) has no return type specified\\.$#"
-			count: 1
-			path: tests/lib/Repository/Values/User/UserTest.php
-
 		-
 			message: "#^Method Ibexa\\\\Tests\\\\Core\\\\Search\\\\Common\\\\FieldValueMapper\\\\RemoteIdentifierMapperTest\\:\\:getDataForTestCanMap\\(\\) return type has no value type specified in iterable type iterable\\.$#"
 			count: 1
diff --git a/src/contracts/Repository/Values/User/User.php b/src/contracts/Repository/Values/User/User.php
index 37de429c59..07457de91c 100644
--- a/src/contracts/Repository/Values/User/User.php
+++ b/src/contracts/Repository/Values/User/User.php
@@ -8,6 +8,7 @@
 
 namespace Ibexa\Contracts\Core\Repository\Values\User;
 
+use DateTimeInterface;
 use Ibexa\Contracts\Core\Repository\Values\Content\Content;
 
 /**
@@ -24,63 +25,36 @@ abstract class User extends Content implements UserReference
     /**
      * @var int[] List of supported (by default) hash types.
      */
-    public const SUPPORTED_PASSWORD_HASHES = [
+    public const array SUPPORTED_PASSWORD_HASHES = [
         self::PASSWORD_HASH_BCRYPT,
         self::PASSWORD_HASH_PHP_DEFAULT,
+        self::PASSWORD_HASH_INVALID,
     ];
 
-    /** @var int Passwords in bcrypt */
-    public const PASSWORD_HASH_BCRYPT = 6;
+    public const int PASSWORD_HASH_BCRYPT = 6;
 
-    /** @var int Passwords hashed by PHPs default algorithm, which may change over time */
-    public const PASSWORD_HASH_PHP_DEFAULT = 7;
+    public const int PASSWORD_HASH_PHP_DEFAULT = 7;
 
-    /** @var int Default password hash, used when none is specified, may change over time */
-    public const DEFAULT_PASSWORD_HASH = self::PASSWORD_HASH_PHP_DEFAULT;
+    public const int PASSWORD_HASH_INVALID = 256;
 
-    /**
-     * User login.
-     *
-     * @var string
-     */
-    protected $login;
+    public const int DEFAULT_PASSWORD_HASH = self::PASSWORD_HASH_PHP_DEFAULT;
 
-    /**
-     * User E-Mail address.
-     *
-     * @var string
-     */
-    protected $email;
+    protected string $login;
 
-    /**
-     * User password hash.
-     *
-     * @var string
-     */
-    protected $passwordHash;
+    protected string $email;
 
-    /**
-     * Datetime of last password update.
-     *
-     * @var \DateTimeInterface|null
-     */
-    protected $passwordUpdatedAt;
+    protected string $passwordHash;
 
-    /**
-     * Hash algorithm used to hash the password.
-     *
-     * @var int
-     */
-    protected $hashAlgorithm;
+    protected ?DateTimeInterface $passwordUpdatedAt;
+
+    protected int $hashAlgorithm;
 
     /**
      * Flag to signal if user is enabled or not.
      *
-     * User can not login if false
-     *
-     * @var bool
+     * User cannot login if false
      */
-    protected $enabled = false;
+    protected bool $enabled = false;
 
     /**
      * Max number of time user is allowed to login.
@@ -90,7 +64,7 @@ abstract class User extends Content implements UserReference
      *
      * @var int
      */
-    protected $maxLogin;
+    protected int $maxLogin;
 
     public function getUserId(): int
     {
diff --git a/src/lib/Repository/User/Exception/UnsupportedPasswordHashType.php b/src/lib/Repository/User/Exception/UnsupportedPasswordHashType.php
index 104a3cf7ce..b71aecf7ac 100644
--- a/src/lib/Repository/User/Exception/UnsupportedPasswordHashType.php
+++ b/src/lib/Repository/User/Exception/UnsupportedPasswordHashType.php
@@ -10,7 +10,7 @@
 
 use Ibexa\Core\Base\Exceptions\InvalidArgumentException;
 
-class UnsupportedPasswordHashType extends InvalidArgumentException
+final class UnsupportedPasswordHashType extends InvalidArgumentException
 {
     public function __construct(int $hashType)
     {
diff --git a/src/lib/Repository/User/PasswordHashService.php b/src/lib/Repository/User/PasswordHashService.php
index f0bebb85dd..6f6edd8f30 100644
--- a/src/lib/Repository/User/PasswordHashService.php
+++ b/src/lib/Repository/User/PasswordHashService.php
@@ -8,16 +8,16 @@
 
 namespace Ibexa\Core\Repository\User;
 
+use Ibexa\Contracts\Core\Repository\PasswordHashService as APIPasswordHashService;
 use Ibexa\Contracts\Core\Repository\Values\User\User;
 use Ibexa\Core\Repository\User\Exception\UnsupportedPasswordHashType;
 
 /**
  * @internal
  */
-final class PasswordHashService implements PasswordHashServiceInterface
+final class PasswordHashService implements APIPasswordHashService
 {
-    /** @var int */
-    private $defaultHashType;
+    private int $defaultHashType;
 
     public function __construct(int $hashType = User::DEFAULT_PASSWORD_HASH)
     {
@@ -56,6 +56,9 @@ public function createPasswordHash(
             case User::PASSWORD_HASH_PHP_DEFAULT:
                 return password_hash($password, PASSWORD_DEFAULT);
 
+            case User::PASSWORD_HASH_INVALID:
+                return '';
+
             default:
                 throw new UnsupportedPasswordHashType($hashType);
         }
@@ -68,7 +71,11 @@ public function isValidPassword(
         string $passwordHash,
         ?int $hashType = null
     ): bool {
-        if ($hashType === User::PASSWORD_HASH_BCRYPT || $hashType === User::PASSWORD_HASH_PHP_DEFAULT) {
+        if (
+            $hashType === User::PASSWORD_HASH_BCRYPT
+            || $hashType === User::PASSWORD_HASH_PHP_DEFAULT
+            || $hashType === User::PASSWORD_HASH_INVALID
+        ) {
             // In case of bcrypt let PHP's password functionality do its magic
             return password_verify($plainPassword, $passwordHash);
         }
diff --git a/src/lib/Repository/User/PasswordHashServiceInterface.php b/src/lib/Repository/User/PasswordHashServiceInterface.php
deleted file mode 100644
index 9d23de7744..0000000000
--- a/src/lib/Repository/User/PasswordHashServiceInterface.php
+++ /dev/null
@@ -1,19 +0,0 @@
-<?php
-
-/**
- * @copyright Copyright (C) Ibexa AS. All rights reserved.
- * @license For full copyright and license information view LICENSE file distributed with this source code.
- */
-declare(strict_types=1);
-
-namespace Ibexa\Core\Repository\User;
-
-use Ibexa\Contracts\Core\Repository\PasswordHashService;
-
-/**
- * @deprecated since Ibexa 3.3.0, to be removed in Ibexa 4.0.0. Use
- * {@see \Ibexa\Contracts\Core\Repository\PasswordHashService} directly instead.
- */
-interface PasswordHashServiceInterface extends PasswordHashService
-{
-}
diff --git a/tests/lib/Repository/User/PasswordHashServiceTest.php b/tests/lib/Repository/User/PasswordHashServiceTest.php
index 384ec84efa..80aaedee8e 100644
--- a/tests/lib/Repository/User/PasswordHashServiceTest.php
+++ b/tests/lib/Repository/User/PasswordHashServiceTest.php
@@ -15,10 +15,9 @@
 
 final class PasswordHashServiceTest extends TestCase
 {
-    private const NON_EXISTING_PASSWORD_HASH = PHP_INT_MAX;
+    private const int NON_EXISTING_PASSWORD_HASH = PHP_INT_MAX;
 
-    /** @var \Ibexa\Core\Repository\User\PasswordHashService */
-    private $passwordHashService;
+    private PasswordHashService $passwordHashService;
 
     protected function setUp(): void
     {
@@ -31,6 +30,7 @@ public function testGetSupportedHashTypes(): void
             [
                 User::PASSWORD_HASH_BCRYPT,
                 User::PASSWORD_HASH_PHP_DEFAULT,
+                User::PASSWORD_HASH_INVALID,
             ],
             $this->passwordHashService->getSupportedHashTypes()
         );
diff --git a/tests/lib/Repository/Values/User/UserTest.php b/tests/lib/Repository/Values/User/UserTest.php
index af686d7c57..ada2c2df8c 100644
--- a/tests/lib/Repository/Values/User/UserTest.php
+++ b/tests/lib/Repository/Values/User/UserTest.php
@@ -18,34 +18,11 @@
 /**
  * @covers \Ibexa\Core\Repository\Values\User\User
  */
-class UserTest extends TestCase
+final class UserTest extends TestCase
 {
     use ValueObjectTestTrait;
 
-    /**
-     * Test a new class and default values on properties.
-     */
-    public function testNewClass()
-    {
-        $user = new User();
-
-        $this->assertPropertiesCorrect(
-            [
-                'login' => null,
-                'email' => null,
-                'passwordHash' => null,
-                'hashAlgorithm' => null,
-                'maxLogin' => null,
-                'enabled' => false,
-            ],
-            $user
-        );
-    }
-
-    /**
-     * Test getName method.
-     */
-    public function testGetName()
+    public function testGetName(): void
     {
         $name = 'Translated name';
         $contentMock = $this->createMock(Content::class);
@@ -64,10 +41,7 @@ public function testGetName()
         self::assertEquals($name, $object->getName());
     }
 
-    /**
-     * Test retrieving missing property.
-     */
-    public function testMissingProperty()
+    public function testMissingProperty(): void
     {
         $this->expectException(PropertyNotFoundException::class);
 
@@ -76,7 +50,7 @@ public function testMissingProperty()
         self::fail('Succeeded getting non existing property');
     }
 
-    public function testObjectProperties()
+    public function testObjectProperties(): void
     {
         $object = new User();
         $properties = $object->attributes();
@@ -98,22 +72,7 @@ public function testObjectProperties()
         }
     }
 
-    /**
-     * Test setting read only property.
-     */
-    public function testReadOnlyProperty()
-    {
-        $this->expectException(PropertyReadOnlyException::class);
-
-        $user = new User();
-        $user->login = 'user';
-        self::fail('Succeeded setting read only property');
-    }
-
-    /**
-     * Test if property exists.
-     */
-    public function testIsPropertySet()
+    public function testIsPropertySet(): void
     {
         $user = new User();
         $value = isset($user->notDefined);
@@ -123,10 +82,7 @@ public function testIsPropertySet()
         self::assertTrue($value);
     }
 
-    /**
-     * Test unsetting a property.
-     */
-    public function testUnsetProperty()
+    public function testUnsetProperty(): void
     {
         $this->expectException(PropertyReadOnlyException::class);