Skip to content

Next major: add preserveImpersonation option to setIdentity() / clearIdentity() #797

Description

@dereuromark

Background

PR #788 added AuthenticationComponent::replaceIdentity() for the common request-only identity refresh case (e.g. attaching eager-loaded associations in beforeFilter() without ending an active impersonation).

An earlier iteration of that PR also expanded two existing signatures:

  • AuthenticationComponent::setIdentity(ArrayAccess|array $identity, bool $preserveImpersonation = false)
  • AuthenticationService::clearIdentity(ServerRequestInterface $request, ResponseInterface $response, bool $stopImpersonation = true)

Those were reverted before merge to keep the public API surface unchanged on the 4.x minor. The signature growth belongs in the next major, where adding the flag to PersistenceInterface::clearIdentity() itself is clean.

Proposal for the next major

Add a first-class "persist a refreshed identity while keeping impersonation alive" path. Unlike replaceIdentity() (request attribute only, lost on the next request), this survives into subsequent requests.

1. Extend the interface

// PersistenceInterface
public function clearIdentity(
    ServerRequestInterface $request,
    ResponseInterface $response,
    bool $stopImpersonation = true,
): array;

2. Honor the flag in AuthenticationService::clearIdentity()

public function clearIdentity(
    ServerRequestInterface $request,
    ResponseInterface $response,
    bool $stopImpersonation = true,
): array {
    foreach ($this->authenticators() as $authenticator) {
        if ($authenticator instanceof PersistenceInterface) {
            if (
                $stopImpersonation
                && $authenticator instanceof ImpersonationInterface
                && $authenticator->isImpersonating($request)
            ) {
                $stopImpersonationResult = $authenticator->stopImpersonating($request, $response);
                ['request' => $request, 'response' => $response] = $stopImpersonationResult;
            }
            $result = $authenticator->clearIdentity($request, $response);
            ['request' => $request, 'response' => $response] = $result;
        }
    }
    if ($stopImpersonation) {
        $this->_successfulAuthenticator = null;
    }

    return [
        'request' => $request->withoutAttribute($this->getConfig('identityAttribute')),
        'response' => $response,
    ];
}

3. Add the opt-in to the component

public function setIdentity(ArrayAccess|array $identity, bool $preserveImpersonation = false)
{
    $controller = $this->getController();
    $service = $this->getAuthenticationService();

    $service->clearIdentity(
        $controller->getRequest(),
        $controller->getResponse(),
        stopImpersonation: !$preserveImpersonation,
    );

    /** @var array{request: \Cake\Http\ServerRequest, response: \Cake\Http\Response} $result */
    $result = $service->persistIdentity(
        $controller->getRequest(),
        $controller->getResponse(),
        $identity,
    );

    $controller->setRequest($result['request']);
    $controller->setResponse($result['response']);

    return $this;
}

Usage

// Refresh the impersonated user and keep it across requests, without
// reverting to the impersonator:
$this->Authentication->setIdentity($reloaded, preserveImpersonation: true);

Note this also skips the session rotation that the default setIdentity() flow performs - it is a refresh, not a privilege transition, so it must not be used for login or role changes. That caveat should be documented.

Notes

  • Because clearIdentity() is on PersistenceInterface, adding the parameter there is a hard BC break and is why this is deferred to the major.
  • Tests for both the preserveImpersonation: true persist case and the default-ends-impersonation case existed in the PR branch history and can be lifted from there.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions