Building a Drupal module that hooks into user login events
Drupal provides a supported way to react when an account is authenticated. A small custom module can implement hook_user_login(), receive the authenticated account and delegate the work to a service. This creates a clean boundary between Drupal’s login process and actions such as audit logging, welcome messages or downstream membership updates.
The hook runs at a successful login transition, not on every authenticated page request. It receives an account object, never the submitted password. Keeping those facts clear prevents accidental credential logging and duplicate messages when an existing session continues to be used.
The example below targets Drupal 10.3 and 11, uses dependency injection for the handler and treats external calls cautiously. It also accounts for Australian time zones, privacy obligations and the mixed login paths commonly found on sites maintained by Australian Drupal teams.
Preparing a focused custom module
Start with a dedicated module rather than adding login behaviour to an existing feature. A module such as ausport_login can contain the hook, its service and configuration without coupling authentication to unrelated editorial functionality. The minimum structure is straightforward.
modules/custom/ausport_login/
├── ausport_login.info.yml
├── ausport_login.module
├── ausport_login.services.yml
└── src/
└── LoginObserver.php
The information file identifies the supported core versions and gives administrators a useful description in the module list. It should not contain secrets or environment-specific values.
name: 'AU Login Observer'
type: module
description: 'Records selected successful user login events.'
core_version_requirement: ^10.3 || ^11
Keep the business logic in a service, with the procedural hook acting as the adapter between Drupal and that service. The custom module guide demonstrates the same directory-and-service pattern with a contact form, which gives teams a consistent starting point.
Implementing the login hook
Drupal names hook implementations by combining the module name with the hook name. For the ausport_login module, the implementation is ausport_login_user_login(). The authenticated account is passed to the service, where the required action can be performed.
<?php
use Drupal\Core\Session\AccountInterface;
/**
* Implements hook_user_login().
*/
function ausport_login_user_login(AccountInterface $account): void {
\Drupal::service('ausport_login.login_observer')
->onLogin($account);
}
The service container is appropriate in this small procedural bridge because hook functions do not receive constructor injection. Dependencies used by the handler itself should still be injected through the service class, making the handler straightforward to test and replace.
<?php
namespace Drupal\ausport_login;
use Drupal\Core\Session\AccountInterface;
use Psr\Log\LoggerInterface;
final class LoginObserver {
public function __construct(
private readonly LoggerInterface $logger,
) {}
public function onLogin(AccountInterface $account): void {
$this->logger->info('Account @uid logged in.', [
'@uid' => $account->id(),
]);
}
}
Register both the logger channel and observer service in ausport_login.services.yml.
services:
logger.channel.ausport_login:
parent: logger.channel_base
arguments: ['ausport_login']
ausport_login.login_observer:
class: Drupal\ausport_login\LoginObserver
arguments:
- '@logger.channel.ausport_login'
Logging the account identifier is safer than logging the username, email address or IP address, although even an identifier can become personal information in some contexts. The hook itself receives no password, token or authentication credential, so the implementation should not attempt to recover them.
Choosing the right integration point
The correct integration point depends on when the action must occur and whether the site supports authentication methods beyond the standard form.
| Approach | When it runs | Suitable use | Important limitation |
|---|---|---|---|
hook_user_login() |
After Drupal finalises a successful login | Audit records, welcome messages and post-login workflows | Does not run for rejected credentials or every authenticated request |
| Login form alteration | While the core login form is being built | Form fields, messages and form-specific redirects | Can miss one-time links and external authentication routes |
| Authentication provider | During credential validation | Custom protocols or specialised authentication services | Security-sensitive code that must integrate carefully with core |
| Event subscriber | When the application dispatches a matching event | Decoupling custom workflows from core login code | Covers only events that the application actually emits |
Use hook_user_login() for actions that belong specifically to a completed login. The standard Drupal login form normally passes through the relevant core login-finalisation path. Core one-time login links generally do as well, but an external single sign-on module must be checked because it may use a different route.
Form alteration is weaker as a general login notification mechanism because it cannot observe every successful authentication path. It remains useful for changing the login experience itself, such as adding explanatory text or adjusting a redirect. An authentication provider belongs in the credential-verification stage and should be reserved for protocols that the standard form cannot handle.
Accounting for Australian deployment realities
Australian sites often operate across several time zones. Sydney and Melbourne normally use AEST or AEDT, Brisbane uses AEST, Adelaide follows AEST or ACDT, and Perth uses AWST. Store event timestamps in UTC, retain Drupal’s machine-readable log timestamps and render them in the site or user’s configured time zone. A single unqualified timestamp such as 09:30 can be misleading for support teams coordinating issues across Australian cities.
Where the Privacy Act 1988 applies, the Australian Privacy Principles influence how login data is collected, stored and disclosed. IP addresses, user-agent details and stable account identifiers may be relevant when assessing personal information. Log only what the event needs, restrict access to that data, define a retention period and avoid copying authentication records into marketing systems. The Notifiable Data Breaches scheme may also affect how eligible organisations investigate a compromised account repository.
Login-triggered emails require separate attention. Commercial electronic messages covered by the Spam Act 2003 generally need an appropriate basis, accurate sender identification and a working unsubscribe mechanism. A transactional security notice is different from a promotional message, but sending it through a general campaign tool can undermine that distinction. A dedicated service with consent-aware templates is safer than reusing a newsletter workflow.
Local content conventions also affect the implementation. Australian organisations commonly use .com.au or direct .au domains, spell terms such as “organisation” and “authorisation” in Australian English and display dates as day-month-year. Australian Drupal agencies, universities, councils and not-for-profits may also work with distributed teams in Sydney, Melbourne, Brisbane and Perth, so support procedures should identify the relevant Australian business hours for urgent account issues.
Budgets in the local market are commonly quoted in Australian dollars and include 10% GST. This matters when the module feeds membership counts, renewal revenue or CRM records. Currency should be stored in the smallest reliable unit, such as cents, rather than as a formatted string, and exchange-rate handling should remain outside the login hook unless the workflow genuinely requires it.
Testing and operating the login hook
A login hook may appear simple while covering several authentication paths. The following checks distinguish a successful login event from a request made by an already authenticated session.
Before rollout
- Map every enabled login path, including core forms, one-time links and external single sign-on providers.
- Define the minimum account data required and document its purpose and retention period.
- Restrict access to log records and confirm that credentials, tokens and unnecessary personal details are excluded.
- Place outbound notifications in a queue so a slow third-party service cannot block authentication.
During verification
- Exercise valid and invalid credentials and confirm that only the valid attempt invokes the hook.
- Test blocked, locked and password-reset accounts to verify the expected lifecycle behaviour.
- Inspect logs as an administrator and compare event times with Australian and UTC display settings.
- Repeat a login after an existing session has been established to confirm that the action is not duplicated unnecessarily.
Use a dedicated test account with representative roles rather than a real person’s account. Automated tests can call the service directly for business rules, while a browser test should confirm that the complete Drupal login path invokes the hook correctly. Also test a failed login: the absence of a log record is as important as the presence of a successful one.
An external membership or CRM update should be idempotent because network retries can process the same login event again. A stable account identifier plus the event time can form part of an idempotency key, while a queued job should record failures for controlled retry. The hook should return quickly, leaving slow work outside the authentication request.
A well-bounded login hook remains a compact and auditable integration point when it observes successful authentication, avoids credential handling and produces records that are proportionate, testable and consistent with Australian privacy expectations.