With this Symfony bundle you can send an email alert when a user logs in from a new context — for example:
- a different IP address
- a different location (geolocation)
- a different User Agent (device/browser)
This helps detect unusual login activity early and increases visibility into authentication events.
Upgrading from v1? See the UPGRADE.md guide for a step-by-step migration.
To ensure strong authentication security, this bundle aligns with guidance from the OWASP Authentication Cheat Sheet by:
- Treating authentication failures or unusual logins as events worthy of detection and alerting
- Ensuring all login events are logged, especially when the context changes (IP, location, device)
- Using secure channels (TLS) for all authentication-related operations
- Validating and normalizing incoming data (e.g. user agent strings, IP addresses) to avoid ambiguity or spoofing
- Authentication Event Logging: Track successful logins with IP, user agent, timestamp and location
- Geolocation Support: Enrich logs with location data using GeoIP2 or IP API
- Email Notifications: Automatically alert users when a login from an unknown context is detected
- Messenger Integration: Optional async processing with Symfony Messenger
- Repository-Based Persistence: No factory or listener boilerplate — implement two interfaces in your repository and you're done
- Extensible: Replace the default email notification with any custom transport via
NotificationInterface
composer require spiriitlabs/auth-log-bundle# config/packages/spiriit_auth_log.yaml
spiriit_auth_log:
transports:
sender_email: 'no-reply@yourdomain.com'
sender_name: 'Security'AuthLogUserInterface extends UserInterface, so you no longer need to declare it explicitly.
use Spiriit\Bundle\AuthLogBundle\Entity\AuthLogUserInterface;
class User implements AuthLogUserInterface
{
// ... your existing User fields
public function getAuthLogEmail(): string
{
return $this->email;
}
public function getAuthLogDisplayName(): string
{
return $this->name;
}
}Extend AbstractAuthenticationLog and add a relation to your User entity:
use Doctrine\ORM\Mapping as ORM;
use Spiriit\Bundle\AuthLogBundle\Entity\AbstractAuthenticationLog;
use Spiriit\Bundle\AuthLogBundle\Entity\AuthLogUserInterface;
use Spiriit\Bundle\AuthLogBundle\FetchUserInformation\UserInformation;
#[ORM\Entity(repositoryClass: UserAuthLogRepository::class)]
class UserAuthLog extends AbstractAuthenticationLog
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(targetEntity: User::class)]
private User $user;
public function __construct(User $user, UserInformation $userInformation)
{
$this->user = $user;
parent::__construct($userInformation);
}
public function getUser(): AuthLogUserInterface
{
return $this->user;
}
}Your repository must implement two interfaces:
AuthenticationLogRepositoryInterface— check if a log already exists and save new logsAuthenticationLogCreatorInterface— build the log entity from a user identifier and user information
use Doctrine\ORM\EntityRepository;
use Spiriit\Bundle\AuthLogBundle\AuthenticationLog\AuthenticationLogCreatorInterface;
use Spiriit\Bundle\AuthLogBundle\Entity\AbstractAuthenticationLog;
use Spiriit\Bundle\AuthLogBundle\FetchUserInformation\UserInformation;
use Spiriit\Bundle\AuthLogBundle\Repository\AuthenticationLogRepositoryInterface;
class UserAuthLogRepository extends EntityRepository implements
AuthenticationLogRepositoryInterface,
AuthenticationLogCreatorInterface
{
public function save(AbstractAuthenticationLog $log): void
{
$this->getEntityManager()->persist($log);
$this->getEntityManager()->flush();
}
public function findExistingLog(string $userIdentifier, UserInformation $userInformation): bool
{
return null !== $this->findOneBy([
'user' => $userIdentifier,
'ipAddress' => $userInformation->ipAddress,
]);
}
public function createLog(string $userIdentifier, UserInformation $userInformation): AbstractAuthenticationLog
{
$user = $this->getEntityManager()->getRepository(User::class)->findOneBy([
'email' => $userIdentifier,
]);
return new UserAuthLog($user, $userInformation);
}
}That's it! The bundle automatically listens to LoginSuccessEvent, checks if the login context is known, persists the log, and sends a notification email when a new context is detected.
GeoIP2 (local database):
spiriit_auth_log:
location:
provider: 'geoip2'
geoip2_database_path: '%kernel.project_dir%/var/GeoLite2-City.mmdb'IP API (external API, 45 req/min free):
spiriit_auth_log:
location:
provider: 'ipApi'spiriit_auth_log:
messenger: 'messenger.default_bus'Optional routing:
framework:
messenger:
routing:
'Spiriit\Bundle\AuthLogBundle\Messenger\AuthLoginMessage\AuthLoginMessage': asyncWhen a new device/context is detected, the bundle dispatches a AuthenticationLogEvents::NEW_DEVICE event. You can listen to it for custom processing (logging, analytics, etc.):
use Spiriit\Bundle\AuthLogBundle\Listener\AuthenticationLogEvent;
use Spiriit\Bundle\AuthLogBundle\Listener\AuthenticationLogEvents;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: AuthenticationLogEvents::NEW_DEVICE)]
final class NewDeviceListener
{
public function __invoke(AuthenticationLogEvent $event): void
{
$userIdentifier = $event->userIdentifier();
$userInformation = $event->userInformation();
// your custom logic here
}
}Note: Persistence and notification are handled automatically by the bundle. You do not need to listen to this event for the bundle to work.
By default, the bundle sends email alerts via Symfony Mailer. To use a different transport (Slack, SMS, etc.), implement NotificationInterface and register it as a service:
use Spiriit\Bundle\AuthLogBundle\DTO\UserReference;
use Spiriit\Bundle\AuthLogBundle\FetchUserInformation\UserInformation;
use Spiriit\Bundle\AuthLogBundle\Notification\NotificationInterface;
final class SlackNotification implements NotificationInterface
{
public function send(UserInformation $userInformation, UserReference $userReference): void
{
// send a Slack message, SMS, etc.
}
}Then point the mailer transport to your service ID:
spiriit_auth_log:
transports:
mailer: 'App\Notification\SlackNotification'
sender_email: 'no-reply@yourdomain.com'
sender_name: 'Security'You can override the default email template:
Create the file:
templates/bundles/SpiriitAuthLogBundle/new_device.html.twig
Available variables in the template:
| Variable | Type | Description |
|---|---|---|
userInformation.ipAddress |
?string |
Client IP address |
userInformation.userAgent |
?string |
Browser / device user agent |
userInformation.loginAt |
?DateTimeImmutable |
Login timestamp |
userInformation.location |
?LocateValues |
Geolocation (city, country, latitude, longitude) |
authenticableLog.displayName |
string |
User display name |
authenticableLog.email |
string |
User email |
Internal flow when a user logs in:
LoginListenercatches Symfony'sLoginSuccessEvent- Builds a
LoginParameterDtofrom the request (IP, user agent, user identifier) - Dispatches to
LoginService(sync) orAuthLoginMessage(async via Messenger) LoginServicefetches geolocation data viaFetchUserInformationDoctrineAuthenticationLogHandlerchecks if the context is known (findExistingLog), and if not, creates and saves the log (createLog+save)- Dispatches
AuthenticationLogEvents::NEW_DEVICEevent - Sends notification via
NotificationInterface
composer test # Run the test suite
composer cs-check # Check code style (dry-run)
composer cs-fix # Fix code style
vendor/bin/phpstan analyse # Static analysisContributions are welcome! Please feel free to submit a Pull Request
This bundle is released under the MIT License. See the LICENSE file for details.
For questions and support, please contact dev@spiriit.com or open an issue on GitHub.
