adt / doctrine-authenticator
A Doctrine authenticator for Nette framework.
Requires
- php: >=8.4
- adt/doctrine-components: ^3.3
- brick/phonenumber: ^0.7
- doctrine/orm: ^2.9|^3.0
- nette/di: ^3.1
- nette/http: ^3.0
- nette/security: ^3.2
- symfony/console: ^6.0|^7.0|^8.0
Suggests
- geoip2/geoip2: Country-based fraud detection (setCountryFraudDetection)
This package is auto-updated.
Last update: 2026-08-31 11:13:43 UTC
README
- Allows you to use a Doctrine entity as a Nette identity
- Uses cookies instead of PHP sessions
- Saves IP address and User-Agent header for better abuse detection
- Detects an invalid token and call onInvalidToken callback to log and prevent possible abuse
- Invalidates token on different User-Agent header and IP address when fraudDetection is enabled and call onFraudDetection callback to log and prevent possible abuse
Install
composer require adt/doctrine-authenticator
Configuration
1) Neon configuration
services: security.user: App\Model\Security\SecurityUser security.userStorage: Nette\Bridges\SecurityHttp\CookieStorage security.authenticator: factory: App\Model\Security\Authenticator(expiration: '14 days') setup: - setFraudDetection(true) # you can disable it for automatic tests for example - setAuthLog(true) # opt-in append-only audit trail, see "Auth log" below
Add new mapping via attributes like this (if you are using nettrine):
nettrine.orm.attributes: mapping: ADT\DoctrineAuthenticator: %appDir%/../vendor/adt/doctrine-authenticator/src
or via annotations:
nettrine.orm.annotations: mapping: ADT\DoctrineAuthenticator: %appDir%/../vendor/adt/doctrine-authenticator/src
2) Create a Identity entity implementing ADT\DoctrineAuthenticator\DoctrineAuthenticatorIdentity
and adjust to your needs.
<?php namespace App\Model\Entities; use ADT\DoctrineAuthenticator\DoctrineAuthenticatorIdentity; use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\GeneratedValue; use Doctrine\ORM\Mapping\Id; /** @Entity */ #[Entity] class Identity implements DoctrineAuthenticatorIdentity { /** * @Id * @Column * @GeneratedValue */ #[Id] #[Column] #[GeneratedValue] protected ?int $id; public function getId(): int { return $this->id; } public function __clone() { $this->id = null; } /** @Column(unique=true) */ #[Column(unique: true)] protected string $email; /** @Column */ #[Column] protected string $password; public function getEmail(): string { return $this->email; } public function setEmail(string $email): self { $this->email = $email; return $this; } public function getPassword(): string { return $this->password; } public function setPassword(string $password): self { $this->password = $password; return $this; } public function getRoles(): array { return []; } public function getAuthObjectId(): string { return (string) $this->getId(); } }
3) Create a SecurityUser service extending ADT\DoctrineAuthenticator\SecurityUser
<?php namespace App\Model\Security; use App\Model\Entities\Identity; /** * @method Identity getIdentity() */ class SecurityUser extends \ADT\DoctrineAuthenticator\SecurityUser { }
4) Create Authenticator extending ADT\DoctrineAuthenticator\DoctrineAuthenticator
and adjust methods authenticate and getIdentity to your needs.
<?php namespace App\Model\Security; use ADT\DoctrineAuthenticator\DoctrineAuthenticator; use App\Model\Entities\Identity; use Doctrine\DBAL\Connection; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManagerInterface; use Nette\Bridges\SecurityHttp\CookieStorage; use Nette\Http\Request; use Nette\Security\AuthenticationException; use Nette\Security\IIdentity; use Nette\Security\Passwords; class Authenticator extends DoctrineAuthenticator { public function __construct( string $expiration, CookieStorage $cookieStorage, Connection $connection, Configuration $configuration, Request $httpRequest, protected readonly EntityManagerInterface $em, ) { parent::__construct($expiration, $cookieStorage, $connection, $configuration, $httpRequest); $this->onInvalidToken = function(string $token) { // log probable fraud }; } public function authenticate(string $user, string $password): IIdentity { /** @var Identity $identity */ if (! $identity = $this->em->getRepository(Identity::class)->findOneBy(['email' => $user])) { throw new AuthenticationException('Identity not found!'); } if (!(new Passwords())->verify($password, $identity->getPassword())) { throw new AuthenticationException('Incorrect password!'); } return $identity; } public function getIdentity($id): IIdentity { return $this->em->getRepository(Identity::class)->find($id); } }
5) Generate migrations
for example like this:
php bin/console migrations:diff
Usage
Just call login on security user as you are used to:
$this->securityUser->login($email, $password);
Country-based fraud detection
The default fraud detection kills a session when both the IP and the User-Agent change at once. An attacker who stole the session token can trivially copy the User-Agent, so you can additionally bind the session to a country: any IP change within one country is allowed (mobile networks, CGNAT), moving to a different country kills the session even with a matching User-Agent.
setup: - setCountryFraudDetection('/geoip/GeoLite2-Country.mmdb')
Requires composer require geoip2/geoip2 and a MaxMind Country database.
The recommended way to provide and refresh the .mmdb file is the official
geoipupdate container writing into
a volume mounted read-only into the application container (MaxMind licensing
does not allow bundling the file, and it goes stale - updates are published
twice a week).
The check fails open: an unresolvable IP or a missing/unreadable database
never kills a session, it only disables the country rule (the IP+User-Agent
rule still applies). Detected frauds are recorded in the auth log with reason
country changed (CZ -> US).
Auth log (audit trail)
With setAuthLog(true) the authenticator writes an append-only audit record
into the auth_log table for every authentication event:
| type | when |
|---|---|
login |
successful login (written in the same transaction as the session row) |
login_failed |
failed login - records the entered identity and the exception class |
login_blocked |
attempt rejected by the login-attempt protection |
logout |
session invalidated via clearIdentity() / clearSession() |
fraud_detected |
session killed because IP and User-Agent both changed |
invalid_token |
cookie token not found (metadata contains its sha256 for correlation with session.token) |
Rows are inserted through the DBAL connection (no ORM events, no unit of work)
and are never updated. The table is a staging buffer: the project must
periodically move rows into its long-term audit store (read ORDER BY id,
delete after a confirmed copy) - without that the table grows indefinitely.
Passwords or other credentials are never recorded.
Clearing expired sessions
Register the extension, which registers the console command:
extensions: doctrineAuthenticator: ADT\DoctrineAuthenticator\DI\DoctrineAuthenticatorExtension
It deletes sessions whose validUntil is older than the given number of days
(defaults to 365 days, i.e. one year):
# delete sessions expired more than a year ago (default) php bin/console doctrine-authenticator:clear-expired-sessions # delete sessions expired more than 30 days ago php bin/console doctrine-authenticator:clear-expired-sessions 30
Run it periodically (e.g. via cron) to keep the session table clean.