Skip to content
Draft
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
81 changes: 74 additions & 7 deletions src/Authenticator/PrimaryKeySessionAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,95 @@
namespace Authentication\Authenticator;

use ArrayAccess;
use Authentication\Identifier\IdentifierFactory;
use Authentication\Identifier\IdentifierInterface;
use Cake\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

/**
* Session Authenticator with only ID
*
* This authenticator stores only the user's primary key in the session,
* and looks up the full user record from the database on each request.
*
* By default, it uses a TokenIdentifier configured to look up users by
* their `id` field. This works out of the box for most applications:
*
* ```php
* $service->loadAuthenticator('Authentication.PrimaryKeySession');
* ```
*
* You can customize the identifier configuration if needed:
*
* ```php
* $service->loadAuthenticator('Authentication.PrimaryKeySession', [
* 'identifier' => [
* 'className' => 'Authentication.Token',
* 'tokenField' => 'uuid',
* 'dataField' => 'key',
* 'resolver' => [
* 'className' => 'Authentication.Orm',
* 'userModel' => 'Members',
* ],
* ],
* ]);
* ```
*/
class PrimaryKeySessionAuthenticator extends SessionAuthenticator
{
/**
* @param \Authentication\Identifier\IdentifierInterface|null $identifier
* @param array<string, mixed> $config
* Default config for this object.
*
* - `identifierKey` The key used when passing the ID to the identifier.
* - `idField` The field on the user entity that contains the primary key.
*
* @var array<string, mixed>
*/
protected array $_defaultConfig = [
'fields' => [],
'sessionKey' => 'Auth',
'impersonateSessionKey' => 'AuthImpersonate',
'identify' => false,
'identityAttribute' => 'identity',
'identifierKey' => 'key',
'idField' => 'id',
];

/**
* Constructor
*
* Bypasses SessionAuthenticator's default PasswordIdentifier creation
* to allow lazy initialization of the TokenIdentifier in getIdentifier().
*
* @param \Authentication\Identifier\IdentifierInterface|null $identifier Identifier instance.
* @param array<string, mixed> $config Configuration settings.
*/
public function __construct(?IdentifierInterface $identifier, array $config = [])
{
$config += [
'identifierKey' => 'key',
'idField' => 'id',
];
$this->_identifier = $identifier;
$this->setConfig($config);
}

/**
* Gets the identifier.
*
* If no identifier was explicitly configured, creates a default TokenIdentifier
* configured to look up users by their primary key (`id` field).
*
* @return \Authentication\Identifier\IdentifierInterface
*/
public function getIdentifier(): IdentifierInterface
{
if ($this->_identifier === null) {
$this->_identifier = IdentifierFactory::create([
'className' => 'Authentication.Token',
'tokenField' => $this->getConfig('idField'),
'dataField' => $this->getConfig('identifierKey'),
]);
}

parent::__construct($identifier, $config);
return $this->_identifier;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Authentication\Authenticator\PrimaryKeySessionAuthenticator;
use Authentication\Authenticator\Result;
use Authentication\Identifier\IdentifierFactory;
use Authentication\Identifier\TokenIdentifier;
use Cake\Http\Exception\UnauthorizedException;
use Cake\Http\Response;
use Cake\Http\ServerRequestFactory;
Expand Down Expand Up @@ -67,7 +68,7 @@ public function setUp(): void
}

/**
* Test authentication
* Test authentication with explicit identifier
*
* @return void
*/
Expand All @@ -89,6 +90,63 @@ public function testAuthenticateSuccess()
$this->assertSame(Result::SUCCESS, $result->getStatus());
}

/**
* Test authentication works with default identifier (no explicit configuration)
*
* @return void
*/
public function testAuthenticateSuccessWithDefaultIdentifier()
{
$request = ServerRequestFactory::fromGlobals(['REQUEST_URI' => '/']);

$this->sessionMock->expects($this->once())
->method('read')
->with('Auth')
->willReturn(1);

$request = $request->withAttribute('session', $this->sessionMock);

// No identifier passed - should use the default TokenIdentifier
$authenticator = new PrimaryKeySessionAuthenticator(null);
$result = $authenticator->authenticate($request);

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

/**
* Test getIdentifier returns default TokenIdentifier when none configured
*
* @return void
*/
public function testGetIdentifierReturnsDefaultWhenNotConfigured()
{
$authenticator = new PrimaryKeySessionAuthenticator(null);
$identifier = $authenticator->getIdentifier();

$this->assertInstanceOf(TokenIdentifier::class, $identifier);
$this->assertSame('id', $identifier->getConfig('tokenField'));
$this->assertSame('key', $identifier->getConfig('dataField'));
}

/**
* Test custom idField/identifierKey config propagates to default identifier
*
* @return void
*/
public function testGetIdentifierUsesCustomConfig()
{
$authenticator = new PrimaryKeySessionAuthenticator(null, [
'idField' => 'uuid',
'identifierKey' => 'token',
]);
$identifier = $authenticator->getIdentifier();

$this->assertInstanceOf(TokenIdentifier::class, $identifier);
$this->assertSame('uuid', $identifier->getConfig('tokenField'));
$this->assertSame('token', $identifier->getConfig('dataField'));
}

/**
* Test authentication
*
Expand Down