Audit Log Tutorial

View Source

The 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

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
end

The 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
end

Generate 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
end

That'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
end

Exclude 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
end

Exclude 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
end

Customise 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
end

Configure 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
end

To disable batching entirely (writes happen immediately):

audit_log do
  write_batching do
    enabled? false
  end
end

Configure 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
end

Available 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
end

The IP privacy transformation applies to all IP-related fields in the request metadata:

  • remote_ip - The direct client IP
  • x_forwarded_for - Proxy chain IPs
  • forwarded - Standard forwarded header with IP information

Audit log attributes

Each audit log entry contains:

  • id - Unique identifier for the log entry
  • subject - 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 instance
  • logged_at - When the event occurred
  • action_name - The action that triggered the event
  • status - :success, :failure, or :unknown
  • extra_data - Additional information including:
    • actor - The actor performing the action (if any)
    • tenant - The tenant context (if using multi-tenancy)
    • request - Request metadata
    • params - 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_mode option
  • 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