Skip to content

Upgrading from 3.x to 4.0

Release 4.0 introduces disavowal reactions: when a user clicks "It wasn't me", the bundle now acts instead of only dispatching an event. The default reaction revokes the user's known contexts so the attacker's next login raises a fresh alert — which fixes a real gap in 3.x, where a disavowed context still counted as known and silenced future notifications.

It also fixes a design flaw introduced with the confirmation feature: the log knew which user class it belonged to, but not which user — so the bundle had to reload the User entity to find out. A log is a journal: it must remember, not ask again.

1. New user_identifier column (required, schema migration)

The log now records who it was written for, not only which class. AbstractAuthenticationLog gains a user_identifier column, filled from the UserIdentity your creator already receives — no constructor change on your side.

bash
php bin/console doctrine:migrations:diff

The generated migration adds a NOT NULL column: back-fill existing rows before applying the constraint.

php
$this->addSql('ALTER TABLE user_auth_log ADD user_identifier VARCHAR(255) DEFAULT NULL');
$this->addSql('UPDATE user_auth_log l SET user_identifier = (SELECT u.email FROM "user" u WHERE u.id = l.user_id)');
$this->addSql('ALTER TABLE user_auth_log ALTER COLUMN user_identifier SET NOT NULL');

If some logs point to a since-deleted user, the sub-select yields NULL and the constraint is refused: decide first whether those orphans keep the identifier as an empty string or get deleted. Replace u.email with whatever your getUserIdentifier() returns. Update the index to match the new lookup:

php
#[ORM\Index(columns: ['user_identifier', 'user_class', 'ip_address'])]

Implementing AuthenticationLogInterface without the mapped superclass? Write userIdentity(): UserIdentity yourself — it must return the identity the log was written with, never one rebuilt from the current user row.

The lookup no longer needs the User entity, which removes one query per login:

php
public function findExistingLog(UserIdentity $userIdentity, UserInformation $userInformation): bool
{
    return null !== $this->findOneBy([
        'userIdentifier' => $userIdentity->userIdentifier,
        'userClass' => $userIdentity->userClass,
        'ipAddress' => $userInformation->ipAddress,
    ]);
}

3. DisavowedLogin carries the resolved user

Custom reactions read $disavowedLogin->user instead of calling $disavowedLogin->authenticationLog->getUser(). The executor resolves the user once, before any reaction runs; if it cannot (deleted row, broken relation), every reaction is skipped and the failure is logged.

php
public function react(DisavowedLogin $disavowedLogin): void
{
    $user = $disavowedLogin->user;             // was: $disavowedLogin->authenticationLog->getUser()
    $identity = $disavowedLogin->userIdentity; // now read from the log, not from the user row
}

4. Implement RevocableAuthenticationLogRepositoryInterface (required with confirmation)

The revoke_known_contexts reaction is enabled by default as soon as confirmation.enabled is true. Your repository must implement the new interface, otherwise container compilation fails with an explicit message:

php
use Spiriit\Bundle\AuthLogBundle\DTO\UserIdentity;
use Spiriit\Bundle\AuthLogBundle\Repository\RevocableAuthenticationLogRepositoryInterface;

class UserAuthLogRepository extends EntityRepository implements
    AuthenticationLogRepositoryInterface,
    AuthenticationLogCreatorInterface,
    ConfirmableAuthenticationLogRepositoryInterface,
    RevocableAuthenticationLogRepositoryInterface
{
    public function revokeKnownContexts(UserIdentity $userIdentity): void
    {
        // UPDATE ... SET status = 'revoked'
        // WHERE userIdentifier/userClass match AND status IN ('pending', 'acknowledged')
    }
}

See the feature page for a complete DQL example. If you prefer the 3.x behavior, disable the reaction explicitly:

yaml
spiriit_auth_log:
    confirmation:
        on_disavowal:
            revoke_known_contexts: false

5. Exclude revoked logs from findExistingLog() (required with confirmation)

Revocation is only effective if a revoked or disavowed log no longer makes a context "known". Add a status filter to your query:

php
use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticationLogStatus;

return null !== $this->findOneBy([
    'userIdentifier' => $userIdentity->userIdentifier,
    'userClass' => $userIdentity->userClass,
    'ipAddress' => $userInformation->ipAddress,
    'status' => [AuthenticationLogStatus::PENDING, AuthenticationLogStatus::ACKNOWLEDGED],
]);

6. New AuthenticationLogStatus::REVOKED case

The enum gains a REVOKED = 'revoked' case, and the confirmable trait a revoke() method. The status column already stores strings of up to 20 characters, so no schema migration is needed — but any exhaustive match on AuthenticationLogStatus in your application must handle the new case.

7. Optional reactions and ports

Two opt-in reactions ship with 4.0, each backed by an interface your application implements:

Config keyPort to implement
on_disavowal.invalidate_sessionsSessionInvalidatorInterface
on_disavowal.force_password_resetPasswordResetRequesterInterface

Custom reactions implement DisavowalReactionInterface and are picked up automatically. Details on the feature page.

Built and maintained by Spiriit — released under the MIT License.