Tutorial

Add user anonymization to Phoenix application

Phoenix 1.7 anonymize changeset csrf

When you are running a web application or service that has users, you will at some point deal with users that want to leave the service. It's not only good practice to allow them to do so, but it is also important to either delete or anonymize their identifiable data. This step is crucial to preserve user integrity while ensuring the privacy and security of user data, which is paramount for GDPR compliance for customers from the EU. There are instances where deleting user records from a database is not feasible due to their association with various vital records or due to data retention policies. This tutorial will guide you through the process of anonymizing user data in such scenarios, ensuring that the user's identity is obscured without the need to delete their records outright. Throughout this guide, I will go through step-by-step to add user data anonymization in an Elixir/Phoenix application.

Adding deletion timestamp to users

The initial step involves altering the database schema to introduce a deleted_at field within the users table. This naive datetime field serves as a marker to denote anonymized users without actual data deletion. Also, for future reference we can see when users left the service.

defmodule Tutorial.Repo.Migrations.AddDeletedAtToUsers do
  use Ecto.Migration

  def change do
    alter table(:users) do
      add :deleted_at, :naive_datetime
    end
  end
end

# Note: You should run this migration with `mix ecto.migrate`.

By creating a new migration and executing it, we update our database structure to accommodate the anonymization process.

Updating the Users Context Module

After the migration has been added and the database updated, I also need to add the field in inside the user schema. And also, for this, I am adding a anonymize_changeset that we can call from the users context. This function securely modifies user entries by replacing sensitive information with placeholders and setting the deletet_attimestamp.

# Tutorial.Users.User
schema "users" do
  # Add this inside the schema
  field :deleted_at, :naive_datetime
end

# Add this changeset
def anonymize_changeset(user) do
  change(user)
  |> put_change(:deleted_at, NaiveDateTime.utc_now(:second))
  |> put_change(:email, "anonymized#{System.unique_integer()}")
end

Following the schema update, we extend the Users context module with a new function, anonymize_user, to that in turn uses the changeset from above.

defmodule Tutorial.Users do
  @moduledoc """
  The Users context.
  """
  import Ecto.Query, warn: false
  alias Tutorial.Repo
  alias Tutorial.Users.User

  def anonymize_user(user_id) do
    user = get_user!(user_id)

    User.anonymize_changeset(user)
    |> Repo.update!()
  end

  def list_users do
    User |> where([u], is_nil(u.deleted_at)) |> Repo.all()
  end

  def get_user(id) do
    User |> Repo.get(id) |> where([u], is_nil(u.deleted_at)) |> Repo.all()
  end
end

Filtering Anonymized Users in Queries

It's essential to ensure that anonymized users are excluded from application queries. This involves adapting functions like list_users/0 andget_user/1, integrating conditions to filter out users marked as anonymized.

Remember to update all getters, especially the ones that regards user login and resetting password.

Add Anonymization when user closes the account

In Phoenix projects that use the built-in authentication system, there's often an existing page where users can manage their account settings. We can integrate the anonymization process into the user settings page, allowing users to close their account and start the anonymization process there.

In this step, I am changing the user settings LiveView that is generated with the built in Phx Gen Auth command. And to implement this, I need to do 3 things.

  1. Add a button in the userface that calls the anonymize event in the LiveView.
  2. Add a LiveView javascript hook that signs out the user and redirects to a another page
  3. The LiveView callback that ties it together.

The code for the button in the front looks like this. Note that I added the logout path and redirect paths as data attributes.

<div id="delete-account" phx-hook="Logout" data-logout-path={~p"/users/log_out"} data-redirect-path={~p"/"} class="...">
  <.icon name="hero-information-circle-mini" class="..." />
  <div class="space-y-6">
    <p class="font-bold">Do you want to close your account?</p>

    <.button kind={:danger} phx-click="anonymize" data-confirm="Are you sure?">
      Delete Account
    </.button>
  </div>
</div>

Below you can see how the button could look like. Also note that I added a data-confirm so the user have a step to thing over the decision.

In the LiveView javascript hook, I added a function for calling logout. This will mimic the behaviour when the user manually clicks logout. After the logout function has run, it redirects the user to the start page.

Hooks.Logout = {
  mounted() {
    const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
    const logoutPath = this.el.dataset.logoutPath
    const redirectPath = this.el.dataset.redirectPath

    async function performLogout() {
      await fetch(logoutPath, {
        method: "DELETE",
        headers: {
          "X-CSRF-Token": csrfToken
        }
      })
    }

    this.handleEvent('logout', data => {
      performLogout().then(() => {
        window.location.href = redirectPath
      })
    })
  }
}

The final part here is to implement the LiveView callback that is being triggered when the user has clicked and confirmed their action.

defmodule TutorialWeb.UserSettingsLive do
  use TutorialWeb, :live_view
  alias Tutorial.Users

  def handle_event("anonymize", _, socket) do
    Users.anonymize_user(socket.assigns.current_user.id)

    {
      :noreply,
      socket
      |> push_event("logout", %{})
    }
  end
end

Note that it pushes the logout event to the javascript hook. That means that the logout request is triggered when the the user is anonymized.

Conclusion

User data anonymization is a critical feature for web applications, especially for those aiming to be GDPR compliant. By following the steps outlined in this tutorial, you can implement a system within your Phoenix application that allows for the anonymization of user data without outright deletion, preserving the integrity of associated records. This not only helps in maintaining user privacy but also aids in adhering to data retention policies.

Related Tutorials

Published 29 Jan - 2020
Updated 01 May - 2020

Send events from JS to a LiveView component

Let say you app uses a javascript library that needs to interact with your app. For example a LiveView component. That is possible with the built in..

Published 28 Jan - 2020
Updated 01 May - 2020

Phoenix LiveView and Invalid CSRF token

One issue that is common to run into is a CSRF error when posting some sort of form rendered with LiveView. The issue is that a LiveView component i..