diff --git a/src/Model/Behavior/SocialAccountBehavior.php b/src/Model/Behavior/SocialAccountBehavior.php index f0e9d849..b9285c1d 100644 --- a/src/Model/Behavior/SocialAccountBehavior.php +++ b/src/Model/Behavior/SocialAccountBehavior.php @@ -105,7 +105,7 @@ public function validateAccount($provider, $reference, $token) ->where(['provider' => $provider, 'reference' => $reference]) ->first(); - if (!empty($socialAccount) && $socialAccount->token === $token) { + if (!empty($socialAccount) && $token !== '' && hash_equals((string)$socialAccount->token, (string)$token)) { if ($socialAccount->active) { throw new AccountAlreadyActiveException(__d('cake_d_c/users', 'Account already validated')); } diff --git a/tests/Fixture/SocialAccountsFixture.php b/tests/Fixture/SocialAccountsFixture.php index e9fe4d11..9c732701 100644 --- a/tests/Fixture/SocialAccountsFixture.php +++ b/tests/Fixture/SocialAccountsFixture.php @@ -32,6 +32,7 @@ class SocialAccountsFixture extends TestFixture 'reference' => 'reference-1-1234', 'avatar' => 'Lorem ipsum dolor sit amet', 'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat. Convallis morbi fringilla gravida, phasellus feugiat dapibus velit nunc, pulvinar eget sollicitudin venenatis cum nullam, vivamus ut a sed, mollitia lectus. Nulla vestibulum massa neque ut et, id hendrerit sit, feugiat in taciti enim proin nibh, tempor dignissim, rhoncus duis vestibulum nunc mattis convallis.', + 'link' => 'https://example.com/user-1-fb', 'token' => 'token-1234', 'token_secret' => 'Lorem ipsum dolor sit amet', 'token_expires' => '2015-05-22 21:52:44', @@ -48,6 +49,7 @@ class SocialAccountsFixture extends TestFixture 'reference' => 'reference-1-1234', 'avatar' => 'Lorem ipsum dolor sit amet', 'description' => '', + 'link' => 'https://example.com/user-1-tw', 'token' => 'token-1234', 'token_secret' => 'Lorem ipsum dolor sit amet', 'token_expires' => '2015-05-22 21:52:44', @@ -64,6 +66,7 @@ class SocialAccountsFixture extends TestFixture 'reference' => 'reference-2-1', 'avatar' => 'Lorem ipsum dolor sit amet', 'description' => '', + 'link' => 'https://example.com/user-2-fb', 'token' => 'token-reference-2-1', 'token_secret' => 'Lorem ipsum dolor sit amet', 'token_expires' => '2015-05-22 21:52:44', @@ -80,6 +83,7 @@ class SocialAccountsFixture extends TestFixture 'reference' => 'reference-2-2', 'avatar' => 'Lorem ipsum dolor sit amet', 'description' => '', + 'link' => 'https://example.com/user-2-tw', 'token' => 'token-reference-2-2', 'token_secret' => 'Lorem ipsum dolor sit amet', 'token_expires' => '2015-05-22 21:52:44', @@ -96,6 +100,7 @@ class SocialAccountsFixture extends TestFixture 'reference' => 'reference-2-2', 'avatar' => 'Lorem ipsum dolor sit amet', 'description' => '', + 'link' => 'https://example.com/user-2-tw', 'token' => 'token-reference-2-2', 'token_secret' => 'Lorem ipsum dolor sit amet', 'token_expires' => '2015-05-22 21:52:44', diff --git a/tests/Fixture/UsersFixture.php b/tests/Fixture/UsersFixture.php index 1736bfe9..54a9c509 100644 --- a/tests/Fixture/UsersFixture.php +++ b/tests/Fixture/UsersFixture.php @@ -23,6 +23,16 @@ */ class UsersFixture extends TestFixture { + /** + * Explicit table alias so _schemaFromReflection resolves to + * CakeDC\Users\Model\Table\UsersTable (which overrides additional_data to json type). + * Without this, the fixture namespace plugin-detection fails because + * Plugin::isLoaded() uses forward-slash names while the namespace uses backslashes. + * + * @var string + */ + public string $tableAlias = 'CakeDC/Users.Users'; + /** * Init method * diff --git a/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php index b6fcae94..8694eb97 100644 --- a/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SocialAccountBehaviorTest.php @@ -89,6 +89,25 @@ public function testValidateEmailInvalidToken() $this->Behavior->validateAccount(1, 'reference-1234', 'invalid-token'); } + /** + * Partial token (prefix of the real token) must be rejected — exercises timing-safe comparison + */ + public function testValidateAccountPartialTokenIsRejected() + { + $this->expectException(RecordNotFoundException::class); + // 'token-123' is a prefix of the valid 'token-1234' — must still fail + $this->Behavior->validateAccount(SocialAccountsTable::PROVIDER_FACEBOOK, 'reference-1-1234', 'token-123'); + } + + /** + * Empty string token must be rejected — exercises timing-safe comparison with empty input + */ + public function testValidateAccountEmptyTokenIsRejected() + { + $this->expectException(RecordNotFoundException::class); + $this->Behavior->validateAccount(SocialAccountsTable::PROVIDER_FACEBOOK, 'reference-1-1234', ''); + } + /** * Test validateEmail method */ diff --git a/tests/schema.php b/tests/schema.php index a7f1051b..11f18519 100644 --- a/tests/schema.php +++ b/tests/schema.php @@ -29,9 +29,9 @@ 'provider' => ['type' => 'string', 'length' => 255, 'unsigned' => false, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 'username' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 'reference' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'avatar' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'avatar' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'description' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'link' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'link' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 'token' => ['type' => 'string', 'length' => 500, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 'token_secret' => ['type' => 'string', 'length' => 500, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], @@ -62,15 +62,15 @@ 'token_expires' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'api_token' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], 'activation_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'secret' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], - 'secret_verified' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => false, 'comment' => '', 'precision' => null], + 'secret' => ['type' => 'string', 'length' => 32, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null, 'fixed' => null], + 'secret_verified' => ['type' => 'boolean', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'tos_date' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], - 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => true, 'comment' => '', 'precision' => null], + 'active' => ['type' => 'boolean', 'length' => null, 'null' => false, 'default' => false, 'comment' => '', 'precision' => null], 'is_superuser' => ['type' => 'boolean', 'length' => null, 'unsigned' => false, 'null' => false, 'default' => false, 'comment' => '', 'precision' => null, 'autoIncrement' => null], 'role' => ['type' => 'string', 'length' => 255, 'null' => true, 'default' => 'user', 'comment' => '', 'precision' => null, 'fixed' => null], 'created' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], 'modified' => ['type' => 'datetime', 'length' => null, 'null' => false, 'default' => null, 'comment' => '', 'precision' => null], - 'additional_data' => ['type' => 'text', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], + 'additional_data' => ['type' => 'json', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'last_login' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'lockout_time' => ['type' => 'datetime', 'length' => null, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null], 'login_token' => ['type' => 'string', 'length' => 32, 'null' => true, 'default' => null, 'comment' => '', 'precision' => null],