spiriitlabs / auth-log-bundle
A Symfony bundle to log authentication events
Installs: 5
Dependents: 0
Suggesters: 0
Security: 0
Stars: 4
Watchers: 0
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=8.2
- doctrine/orm: 3.* || ^4.0
- symfony/config: ^6.4|^7.0
- symfony/dependency-injection: ^6.4|^7.0
- symfony/event-dispatcher: ^6.4|^7.0
- symfony/framework-bundle: ^6.0 || ^7.0
- symfony/http-client: ^6.0 || ^7.0
- symfony/mailer: ^6.0 || ^7.0
- symfony/messenger: ^6.4|^7.0
- symfony/security-bundle: ^6.4|^7.0
- symfony/translation: ^7.3
- symfony/translation-contracts: ^1.0|^2.0|^3.0
- symfony/twig-bundle: ^6.4|^7.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.1
- phpstan/phpstan-symfony: ^2.0
- rector/rector: ^2.1
- symfony/phpunit-bridge: ^7.3
- symfony/yaml: ^7.3
Suggests
- geoip2/geoip2: Required if you want to enrich logs with geolocation
README
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.
OWASP Authentication Best Practices
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
Features
- Authentication Event Logging: Track successful logins with detailed information
- Geolocation Support: Enrich logs with location data using GeoIP2 or IP API
- Email Notifications: Send email alerts for authentication events
- Messenger Integration: Optional processing with Symfony Messenger
- Highly Configurable: Flexible configuration options for various use cases
- Extensible: Easy to extend with custom authentication log entities
Requirements
- PHP 8.3 or higher
- Symfony 6.4+ or 7.0+
- Doctrine ORM 3.0+ or 4.0+
Installation
Install the bundle using Composer:
composer require spiriitlabs/auth-log-bundle
If you're using Symfony Flex, the bundle will be automatically registered. Otherwise, add it to your config/bundles.php
:
<?php return [ // ... Spiriit\Bundle\AuthLogBundle\SpiriitAuthLogBundle::class => ['all' => true], ];
Configuration
Create a configuration file config/packages/spiriit_auth_log.yaml
:
Basic Configuration
spiriit_auth_log: # Email notification settings transports: sender_email: 'no-reply@yourdomain.com' sender_name: 'Your App Security'
Configuration with GeoIP2
Using GeoIP2 requires downloading the GeoLite2 database from MaxMind.
spiriit_auth_log: # ... location: provider: 'geoip2' geoip2_database_path: '%kernel.project_dir%/var/GeoLite2-City.mmdb'
Configuration with IP API
ipApi.com offers a free tier with a limit of 45 requests per minute and 1,000 requests per day; exceeding these limits requires a paid plan.
spiriit_auth_log: # ... location: provider: 'ipApi'
Usage
1. Implement AuthenticableLogInterface
Equip your User with AuthenticableLogInterface
:
You could use any entity, here we use a User class as an example.
But it's not an obligation, you have just to implement the interface.
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticableLogInterface; use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity] class User implements UserInterface, AuthenticableLogInterface { // ... your existing User properties and methods public function getAuthenticationLogFactoryName(): string { return 'customer'; // This should match your factory service name } public function getAuthenticationLogsToEmail(): string { return $this->email; } public function getAuthenticationLogsToEmailName(): string { return $this->getFullName(); } }
2. Create Your Authentication Log Entity
Create an entity that extends AbstractAuthenticationLog
:
Here comes the fun part: building your Authentication Log Entity. We will use an User class as an example.
<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; use Spiriit\Bundle\AuthLogBundle\Entity\AbstractAuthenticationLog; use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticableLogInterface; #[ORM\Entity] #[ORM\Table(name: 'user_authentication_logs')] class UserAuthenticationLog extends AbstractAuthenticationLog { #[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column] private ?int $id = null; #[ORM\ManyToOne(targetEntity: User:class)] #[ORM\JoinColumn(nullable: false)] private User $user; public function __construct( User $user, UserInformation $userInformation, ) { $this->user = $user; parent::__construct( userInformation: $userInformation ); } public function getId(): ?int { return $this->id; } public function getUser(): AuthenticableLogInterface { return $this->user; } }
3. Create an Authentication Log Factory
Spin up your Authentication Log Factory:
<?php namespace App\Service; use App\Entity\User; use App\Entity\UserAuthenticationLog; use Spiriit\Bundle\AuthLogBundle\AuthenticationLogFactoryInterface; use Spiriit\Bundle\AuthLogBundle\Entity\AuthenticableLogInterface; use Spiriit\Bundle\AuthLogBundle\FetchUserInformation\UserInformation; class UserAuthenticationLogFactory implements AuthenticationLogFactoryInterface { public function createFrom(string $userIdentifier, UserInformation $userInformation): AbstractAuthenticationLog { $realCustomer = $this->entityManager->getRepository(User::class)->findOneBy(['identifiant' => $userIdentifier]); if (!$realCustomer instanceof User) { throw new \InvalidArgumentException(); } return new UserReference( type: 'customer', id: (string) $realCustomer->getCustomerId(), ); } public function isKnown(UserReference $userReference, UserInformation $userInformation): bool { // Your logic to determine if the authentication log is known // here is an example with Doctrine QueryBuilder // you can also use a different storage system like Redis, ElasticSearch, etc. return (bool) $this->entityManager->createQueryBuilder() ->select('al') ->from(UserAuthenticationLog::class, 'uu') ->innerJoin('uu.user', 'u') ->andWhere('uu.ipAddress = :ip') ->andWhere('uu.userAgent = :ua') ->andWhere('uu.id = :user_id') ->setParameter('user_id', $userReference->id) ->setParameter('ip', $userInformation->ipAddress) ->setParameter('ua', $userInformation->userAgent) ->getQuery() ->getOneOrNullResult() ?? false; } public function supports(AuthenticableLogInterface $authenticableLog): string { return 'customer'; // This should match the value returned by getAuthenticationLogFactoryName() } }
Messenger Integration
To enable a/synchronous processing with Symfony Messenger:
- Configure the bundle:
spiriit_auth_log: messenger: 'messenger.default_bus' # can be your custom service id
- Optional Configure your messenger transports in
config/packages/messenger.yaml
:
By default, the message transport is set to sync
, but you can change it to any transport you prefer:
framework: messenger: routing: 'Spiriit\Bundle\AuthLogBundle\Messenger\AuthLoginMessage\AuthLoginMessage': my_async_transport
Email Notifications
The bundle send email notifications for authentication events.
Currently only LoginSuccessEvent
is supported.
Ensure you have configured Symfony Mailer and enabled notifications:
spiriit_auth_log: transports: mailer: 'mailer' # default is symfony 'mailer' service, you can customize it sender_email: 'security@yourdomain.com' sender_name: 'Security Team'
The parameter mailer accepts any service that implements Spiriit\Bundle\AuthLogBundle\Notification\NotificationInterface
.
Events
The bundle will dispatch an event AuthenticationLogEvents::LOGIN
- your job is to catch it.
Why? Because you decide how the entity gets persisted (the bundle won’t do it for you). Once you’ve saved it, make sure to mark the event as persisted, so the bundle can keep rolling smoothly.
You can listen to these events to add custom logic:
<?php namespace App\EventListener; use Spiriit\Bundle\AuthLogBundle\Listener\AuthenticationLogEvent; use Spiriit\Bundle\AuthLogBundle\Listener\AuthenticationLogEvents; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class CustomAuthenticationLogListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ AuthenticationLogEvents::NEW_DEVICE => 'onLogin', ]; } public function onLogin(AuthenticationLogEvent $event): void { // Add your custom logic here $log = $event->getUserReference(); $userInfo = $event->getUserInformation(); // persist log // flush // !! IMPORTANT !! Make sure to mark the event as persisted to continue the process $event->markAsHandled(); } }
Template
You can use the default template, not recommended indeed!
Override here
templates/bundles/SpiriitAuthLogBundle/new_device.html.twig
You can access to UserInformation object:
The userInformation
object contains details about a user's login session. Each property is optional and may be null or empty.
Properties
ipAddress
- Type:
string | null
- Description: The IP address from which the user logged in.
userAgent
- Type:
string | null
- Description: The browser or device information of the user.
loginAt
- Type:
\DateTimeInterface | null
- Description: The timestamp of the user's login.
location
- Type:
LocateValues | null
- Description: Geographical information about the user's location.
- Properties:
city
(string
) — The city name.country
(string
) — The country name.latitude
(float
) — Latitude coordinate.longitude
(float
) — Longitude coordinate.
Testing
Run the test suite:
vendor/bin/simple-phpunit
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
This bundle is released under the MIT License. See the LICENSE file for details.
Support
For questions and support, please contact dev@spiriit.com or open an issue on GitHub.