Audit Log Tutorial
View SourceThe audit log add-on provides automatic logging of authentication events (sign in, registration, failures, etc.) to help you track security-relevant activities in your application.
Installation
With Igniter (recommended)
Use mix ash_authentication.add_add_on audit_log to automatically set up audit logging:
mix ash_authentication.add_add_on audit_log
This will:
- Create the audit log resource
- Add the add-on to your user resource
- Ensure the AshAuthentication.Supervisor is in your application supervision tree
- Generate and run migrations
You can customise the installation with options:
# Custom audit log resource name
mix ash_authentication.add_add_on audit_log --audit-log MyApp.Accounts.AuthAuditLog
# Include sensitive fields
mix ash_authentication.add_add_on audit_log --include-fields email,username
# Exclude specific strategies
mix ash_authentication.add_add_on audit_log --exclude-strategies magic_link,oauth
# Exclude specific actions
mix ash_authentication.add_add_on audit_log --exclude-actions sign_in_with_token
Manually
If you prefer to set up audit logging manually, continue with the steps below:
Create the audit log resource
First, create a resource to store the audit logs. This resource uses the AshAuthentication.AuditLogResource extension which handles all the necessary setup:
defmodule MyApp.Accounts.AuditLog do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.AuditLogResource],
domain: MyApp.Accounts
postgres do
table "account_audit_logs"
repo MyApp.Repo
end
endThe extension automatically creates all required attributes and actions. You don't need to define any manually unless you want to customise them.
Add the audit log add-on to your user resource
Next, add the audit log add-on to your user resource's authentication configuration:
defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false, public?: true, sensitive?: true
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
end
authentication do
tokens do
enabled? true
token_resource MyApp.Accounts.Token
end
add_ons do
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
end
end
strategies do
password :password do
identity_field :email
end
end
end
identities do
identity :unique_email, [:email]
end
endGenerate and run migrations
Generate migrations for the audit log table:
mix ash.codegen create_accounts_audit_logs
mix ash.migrate
Start the audit log batcher
The audit log uses batched writes to reduce database load. Add the AshAuthentication.Supervisor to your application's supervision tree:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
# Add this line
{AshAuthentication.Supervisor, otp_app: :my_app}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
endThat's it! Authentication events will now be logged automatically.
What gets logged?
The audit log automatically tracks:
- Successful and failed authentication attempts
- User registration events
- The authentication strategy used (password, OAuth2, magic link, etc.)
- The action name that triggered the event
- User subject (when available)
- Timestamp of the event
- Non-sensitive parameters from the request
- Sensitive parameters that are explicitly configured
Viewing audit logs
You can read audit logs like any other Ash resource:
# Get all audit logs
MyApp.Accounts.AuditLog
|> Ash.read!()
# Filter by user
MyApp.Accounts.AuditLog
|> Ash.Query.filter(subject == ^user_subject)
|> Ash.read!()
# Filter by action
MyApp.Accounts.AuditLog
|> Ash.Query.filter(action_name == :sign_in_with_password)
|> Ash.read!()
# Filter by status
MyApp.Accounts.AuditLog
|> Ash.Query.filter(status == :failure)
|> Ash.read!()Configuration options
Include sensitive fields
By default, sensitive arguments and attributes (marked with sensitive?: true) are filtered out of the audit logs. You can explicitly include specific fields:
authentication do
add_ons do
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
include_fields [:email, :username]
end
end
endExclude specific strategies
If you want to exclude certain authentication strategies from being logged:
authentication do
add_ons do
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
exclude_strategies [:magic_link]
end
end
endExclude specific actions
To exclude specific actions from being logged:
authentication do
add_ons do
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
exclude_actions [:sign_in_with_token]
end
end
endCustomise log retention
By default, audit logs are retained for 90 days. You can change this or disable automatic cleanup:
defmodule MyApp.Accounts.AuditLog do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.AuditLogResource],
domain: MyApp.Accounts
audit_log do
# Keep logs for 30 days
log_lifetime 30
# Or disable automatic cleanup
# log_lifetime :infinity
end
postgres do
table "account_audit_log"
repo MyApp.Repo
end
endConfigure write batching
The audit log batches writes to reduce database load. You can customise this behaviour:
defmodule MyApp.Accounts.AuditLog do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.AuditLogResource],
domain: MyApp.Accounts
audit_log do
write_batching do
enabled? true
# Write batch every 5 seconds
timeout :timer.seconds(5)
# Or when batch reaches 50 records
max_size 50
end
end
postgres do
table "account_audit_log"
repo MyApp.Repo
end
endTo disable batching entirely (writes happen immediately):
audit_log do
write_batching do
enabled? false
end
endConfigure IP address privacy
To comply with privacy regulations like GDPR, you can control how IP addresses are stored in audit logs:
authentication do
add_ons do
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
# IP privacy options: :none | :hash | :truncate | :exclude
ip_privacy_mode :truncate
# Network masks for truncation (optional, these are the defaults)
ipv4_truncation_mask 24 # Keep first 3 octets
ipv6_truncation_mask 48 # Keep first 3 segments
end
end
endAvailable IP privacy modes:
:none(default) - Store IP addresses as-is without modification:hash- Hash IP addresses using SHA256 with application secret as salt:truncate- Truncate IP addresses to a network prefix (e.g., 192.168.1.100 → 192.168.1.0/24):exclude- Don't store IP addresses at all
When using :truncate mode, the default masks are:
- IPv4:
/24- Keeps first 3 octets (e.g., 192.168.1.0/24) - IPv6:
/48- Keeps first 3 segments (e.g., 2001:db8:85a3::/48)
Example configurations:
# Hash all IP addresses for privacy
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
ip_privacy_mode :hash
end
# Truncate with more aggressive masking
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
ip_privacy_mode :truncate
ipv4_truncation_mask 16 # Keep first 2 octets (more privacy)
ipv6_truncation_mask 32 # Keep first 2 segments (more privacy)
end
# Exclude IP addresses entirely
audit_log do
audit_log_resource MyApp.Accounts.AuditLog
ip_privacy_mode :exclude
endThe IP privacy transformation applies to all IP-related fields in the request metadata:
remote_ip- The direct client IPx_forwarded_for- Proxy chain IPsforwarded- Standard forwarded header with IP information
Audit log attributes
Each audit log entry contains:
id- Unique identifier for the log entrysubject- The authenticated user's subject string (if available)strategy- The authentication strategy used (:password,:github, etc.)audit_log- The name of the audit log add-on instancelogged_at- When the event occurredaction_name- The action that triggered the eventstatus-:success,:failure, or:unknownextra_data- Additional information including:actor- The actor performing the action (if any)tenant- The tenant context (if using multi-tenancy)request- Request metadataparams- Non-sensitive parameters from the action
resource- The resource module that was authenticated
Security considerations
- Sensitive fields (passwords, tokens, API keys) are automatically filtered from audit logs unless explicitly included via
include_fields - IP addresses can be hashed, truncated, or excluded for privacy compliance using the
ip_privacy_modeoption - Audit logs should be stored in a resilient data layer like PostgreSQL
- Consider setting up alerts for suspicious patterns (multiple failed logins, etc.)
- Ensure proper access controls on the audit log resource using Ash policies
- The audit log resource doesn't have default policies - you should add them based on your security requirements
Example: Adding policies to audit logs
defmodule MyApp.Accounts.AuditLog do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.AuditLogResource],
domain: MyApp.Accounts,
authorizers: [Ash.Policy.Authorizer]
policies do
# Only admins can read audit logs
policy action_type(:read) do
authorize_if relates_to_actor_via([:user, :admin])
end
# Allow AshAuthentication to write logs
policy action_type(:create) do
authorize_if AshAuthentication.Checks.AshAuthenticationInteraction
end
end
postgres do
table "account_audit_log"
repo MyApp.Repo
end
end