Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/Authenticator/CookieAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

use ArrayAccess;
use Authentication\Identifier\AbstractIdentifier;
use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Authentication\PasswordHasher\PasswordHasherTrait;
use Authentication\UrlChecker\UrlCheckerTrait;
use Cake\Http\Cookie\Cookie;
Expand Down Expand Up @@ -55,6 +57,29 @@ class CookieAuthenticator extends AbstractAuthenticator implements PersistenceIn
'salt' => true,
];

/**
* Constructor
*
* @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
* @param array<string, mixed> $config Configuration settings.
*/
public function __construct(IdentifierInterface $identifier, array $config = [])
{
// If no identifier is configured, set up a default Password identifier
if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) {
// Pass the authenticator's fields configuration to the identifier
$identifierConfig = [];
if (isset($config['fields'])) {
$identifierConfig['fields'] = $config['fields'];
}
$identifier = new IdentifierCollection([
'Authentication.Password' => $identifierConfig,
]);
}

parent::__construct($identifier, $config);
}

/**
* @inheritDoc
*/
Expand Down
25 changes: 25 additions & 0 deletions src/Authenticator/FormAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
namespace Authentication\Authenticator;

use Authentication\Identifier\AbstractIdentifier;
use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Authentication\UrlChecker\UrlCheckerTrait;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -47,6 +49,29 @@ class FormAuthenticator extends AbstractAuthenticator
],
];

/**
* Constructor
*
* @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
* @param array<string, mixed> $config Configuration settings.
*/
public function __construct(IdentifierInterface $identifier, array $config = [])
{
// If no identifier is configured, set up a default Password identifier
if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) {
// Pass the authenticator's fields configuration to the identifier
$identifierConfig = [];
if (isset($config['fields'])) {
$identifierConfig['fields'] = $config['fields'];
}
$identifier = new IdentifierCollection([
'Authentication.Password' => $identifierConfig,
]);
}

parent::__construct($identifier, $config);
}

/**
* Checks the fields to ensure they are supplied.
*
Expand Down
25 changes: 25 additions & 0 deletions src/Authenticator/HttpBasicAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
namespace Authentication\Authenticator;

use Authentication\Identifier\AbstractIdentifier;
use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
Expand All @@ -41,6 +43,29 @@ class HttpBasicAuthenticator extends AbstractAuthenticator implements StatelessI
'skipChallenge' => false,
];

/**
* Constructor
*
* @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
* @param array<string, mixed> $config Configuration settings.
*/
public function __construct(IdentifierInterface $identifier, array $config = [])
{
// If no identifier is configured, set up a default Password identifier
if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) {
// Pass the authenticator's fields configuration to the identifier
$identifierConfig = [];
if (isset($config['fields'])) {
$identifierConfig['fields'] = $config['fields'];
}
$identifier = new IdentifierCollection([
'Authentication.Password' => $identifierConfig,
]);
}

parent::__construct($identifier, $config);
}

/**
* Authenticate a user using HTTP auth. Will use the configured User model and attempt a
* login using HTTP auth.
Expand Down
9 changes: 8 additions & 1 deletion src/Authenticator/JwtAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
namespace Authentication\Authenticator;

use ArrayObject;
use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Authentication\Identifier\JwtSubjectIdentifier;
use Cake\Utility\Security;
Expand Down Expand Up @@ -56,7 +57,13 @@ class JwtAuthenticator extends TokenAuthenticator
*/
public function __construct(IdentifierInterface $identifier, array $config = [])
{
parent::__construct($identifier, $config);
// Override parent's default - JWT should use JwtSubject identifier
if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) {
$identifier = new IdentifierCollection(['Authentication.JwtSubject']);
}

// Call TokenAuthenticator's constructor but skip its default
AbstractAuthenticator::__construct($identifier, $config);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be parent::__construct()?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it would trigger the Token ones default I guess.

Copy link
Copy Markdown
Member

@markstory markstory Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm. If we need different behavior perhaps we should undo the inheritance and duplicate some code. We can do that separately too.


if (empty($this->_config['secretKey'])) {
if (!class_exists(Security::class)) {
Expand Down
18 changes: 18 additions & 0 deletions src/Authenticator/TokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
namespace Authentication\Authenticator;

use Authentication\Identifier\IdentifierCollection;
use Authentication\Identifier\IdentifierInterface;
use Authentication\Identifier\TokenIdentifier;
use Psr\Http\Message\ServerRequestInterface;

Expand All @@ -35,6 +37,22 @@ class TokenAuthenticator extends AbstractAuthenticator implements StatelessInter
'tokenPrefix' => null,
];

/**
* Constructor
*
* @param \Authentication\Identifier\IdentifierInterface $identifier Identifier or identifiers collection.
* @param array<string, mixed> $config Configuration settings.
*/
public function __construct(IdentifierInterface $identifier, array $config = [])
{
// If no identifier is configured, set up a default Token identifier
if ($identifier instanceof IdentifierCollection && $identifier->isEmpty()) {
$identifier = new IdentifierCollection(['Authentication.Token']);
}

parent::__construct($identifier, $config);
}

/**
* Checks if the token is in the headers or a request parameter
*
Expand Down
25 changes: 25 additions & 0 deletions tests/TestCase/AuthenticationServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1172,4 +1172,29 @@ public function testIsImpersonatingWrongProvider()
$this->expectExceptionMessage('The Authentication\Authenticator\FormAuthenticator Provider must implement ImpersonationInterface in order to use impersonation.');
$service->isImpersonating($request);
}

/**
* Test that FormAuthenticator works with default Password identifier
*
* @return void
*/
public function testFormAuthenticatorDefaultIdentifier()
{
$request = ServerRequestFactory::fromGlobals(
['REQUEST_URI' => '/testpath'],
[],
['username' => 'mariano', 'password' => 'password'],
);

// Test loading FormAuthenticator without specifying an identifier
$service = new AuthenticationService();
$service->loadAuthenticator('Authentication.Form');

$result = $service->authenticate($request);
$this->assertInstanceOf(Result::class, $result);
$this->assertTrue($result->isValid());

$authenticator = $service->getAuthenticationProvider();
$this->assertInstanceOf(FormAuthenticator::class, $authenticator);
}
}
97 changes: 97 additions & 0 deletions tests/TestCase/Authenticator/FormAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -544,4 +544,101 @@ public function testAuthenticateInvalidChecker()

$form->authenticate($request);
}

/**
* Test that FormAuthenticator uses default Password identifier when none is provided.
*
* @return void
*/
public function testDefaultPasswordIdentifier()
{
// Create an empty IdentifierCollection (simulating no explicit identifier configuration)
$identifiers = new IdentifierCollection();

$request = ServerRequestFactory::fromGlobals(
['REQUEST_URI' => '/testpath'],
[],
['username' => 'mariano', 'password' => 'password'],
);

// FormAuthenticator should automatically configure a Password identifier
$form = new FormAuthenticator($identifiers);
$result = $form->authenticate($request);

$this->assertInstanceOf(Result::class, $result);
$this->assertSame(Result::SUCCESS, $result->getStatus());

// Verify the identifier collection now has the Password identifier
$identifier = $form->getIdentifier();
$this->assertInstanceOf(IdentifierCollection::class, $identifier);
$this->assertFalse($identifier->isEmpty());
}

/**
* Test that FormAuthenticator respects explicitly configured identifier.
*
* @return void
*/
public function testExplicitIdentifierNotOverridden()
{
// Create an IdentifierCollection with a specific identifier
$identifiers = new IdentifierCollection([
'Password' => [
'className' => 'Authentication.Password',
'fields' => [
'username' => 'email',
'password' => 'password',
],
],
]);

ServerRequestFactory::fromGlobals(
['REQUEST_URI' => '/testpath'],
[],
['email' => 'mariano@example.com', 'password' => 'password'],
);

// FormAuthenticator should use the provided identifier
$form = new FormAuthenticator($identifiers);

// The identifier should remain as configured
$identifier = $form->getIdentifier();
$this->assertInstanceOf(IdentifierCollection::class, $identifier);
$this->assertFalse($identifier->isEmpty());
$this->assertSame($identifiers, $identifier, 'Identifier collection should be the same.');
$this->assertSame($identifiers->get('Password'), $identifier->get('Password'), 'Identifier should be the same.');
}

/**
* Test that default identifier inherits fields configuration from authenticator.
*
* @return void
*/
public function testDefaultIdentifierInheritsFieldsConfig()
{
// Create an empty IdentifierCollection
$identifiers = new IdentifierCollection();

// Configure authenticator with custom fields mapping
$config = [
'fields' => [
'username' => 'user_name',
'password' => 'pass_word',
],
];

// FormAuthenticator should create default identifier with inherited fields
$form = new FormAuthenticator($identifiers, $config);

// Verify the identifier was created with the correct configuration
$identifier = $form->getIdentifier();
$this->assertInstanceOf(IdentifierCollection::class, $identifier);
$this->assertFalse($identifier->isEmpty());

// Verify the fields are properly configured
// We can't directly access the internal configuration, but we can verify
// the FormAuthenticator has the expected configuration
$this->assertEquals('user_name', $form->getConfig('fields.username'));
$this->assertEquals('pass_word', $form->getConfig('fields.password'));
}
}