Elixir: Forgot Password

After having deployed your account management system, you’ll (very) soon discover that a good password is a one time (at least) lost password. The remedy is the now common forgot password feature that you’ll need to add soon because sending new passwords is not safe and you’ll not explain to your customers how to setup GPG.

That’s why the easiest way is to define a process for customers to define new password for their account:
  • The user forgot his password
  • It will fill a form with his account email
  • We need to send to him a temporary route (using a temporary token) that he will use to set a new password
  • Because customers will always surprise you, you’ll need to make them validate their new password to be sure it will not be lost again
  • You can then clean the generated token





So how did we made that using Elixir/Phoenix in our application ? We used an awesome feature of Ecto, the database wrapper library provided with Elixir. It allows you to use a DSL in order to create migrations and SQL queries, thus allowing some features like logging model changes before saving them.

The get in depth knowledges about Ecto you should go check the hex documentation that is very good (one of the few sadly). So Ecto is divided in various submodules but we’re going to see only the ones we use on our project. Ecto.Repo is the tool that allows us to make queries to the datastore. It provides known function like Repo.[all, get, create, update, delete…] that you can use for simple queries. For more complicated ones, you need the Ecto.Query module to join, or subquery resources. The Ecto.Multi module allows you to use transactions to rollback complex queries. The Ecto.Migration allows you to write migrations with up and down functions allowing you to rollback a migration if necessary (feels safer). And Ecto.Changeset allows you to store all changes made to a model and/or validate those changes depending on specified constraints.

So let's see some of these constraints to make sure our customer do not bypass our beautiful authentication system. We define them in our User model as is.


defmodule MyApp.Account.User do
  use Ecto.Schema

  import Ecto.Changeset

  alias MyApp.Account.User

  @primary_key {:id, Ecto.UUID, autogenerate: true}

  @derive {Phoenix.Param, key: :id}

  schema "users" do
    field(:email, :string)

    field(:password, :string)

    field(:password_confirmation, :string, virtual: true)

    field(:token, :string)

    field(:reset_password_token, :string)

    field(:reset_token_sent_at, :utc_datetime)

    many_to_many(
      :roles,
      MyApp.Account.Role,
      join_through: "users_roles",
      on_delete: :delete_all,
      on_replace: :delete,
      unique: true
    )

    timestamps()
  end

  def changeset(%User{} = model, attrs \\ :empty) do
    model
    |> cast(attrs, [
      :email,
      :password,
      :password_confirmation,
      :token,
      :reset_password_token,
      :reset_token_sent_at
    ])
    |> unique_constraint(:email)
    |> validate_format(:email, ~r/@/)
    |> validate_required([:email])
  end

  def reset_password_changeset(%User{} = user, params \\ %{}) do
    user
    |> cast(params, [])
    |> put_reset_token()
  end

  defp put_reset_token(changeset) do
    token = SecureRandom.urlsafe_base64()

    sent_at = DateTime.utc_now()

    case changeset do
      %Ecto.Changeset{valid?: true} ->
        changeset
        |> put_change(:reset_password_token, token)
        |> put_change(:reset_token_sent_at, sent_at)

      _ ->
        changeset
    end
  end

  def update_password_changeset(%User{} = user, params \\ %{}) do
    user
    |> cast(params, [:password, :password_confirmation])
    |> validate_length(:password, min: 5)
    |> validate_required([:password, :password_confirmation])
    |> validate_confirmation(:password)
    |> put_pass_hash()
    |> clear_password_reset_token()
  end

  defp put_pass_hash(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
        put_change(changeset, :password, Comeonin.Bcrypt.hashpwsalt(password))

      _ ->
        changeset
    end
  end

  defp clear_password_reset_token(changeset) do
    case changeset do
      %Ecto.Changeset{valid?: true} ->
        changeset
        |> put_change(:reset_password_token, nil)
        |> put_change(:reset_token_sent_at, nil)

      _ ->
        changeset
    end
  end
end

You can see the default changes function that will cast our attributes and validate the mandatory one like the email format or the uniqueness of the email. Note that we also modified default primary key format for our model. Ecto manage integer id but we wanted UUID, you can find more about this in the documentation.

We also define a reset_password_changeset/2 that will set the :reset_password_token and :reset_token_sent_at fields when the user ask for a new password.

These will allow us to check if the token is not too old and to verify that the provided token is correct to be sure that URL will be called only once.

On the update_password/2 controller function side we’ll just get the user based on his token and email, check that the token is less than one day and call the update_password_changeset/2 to put the new hashed password if the password_confirmation is correct of course.

As you can see, you can add complex constraints, custom constraints, on a unique or on multiple fields and also defined virtual fields that are not in the datastore (check carefully the model definition for password_confirmation) using Eco.Changeset. You can also add an explicit error if necessary using Ecto.Changeset.add_error/3.

The library make your code clean for model validation, but this is just the beginning as we are going to implement a change log system for our defined models thanks to the provided Ecto.Changeset in another post.

If you like the post please add a like or a comment so I can improve it as much as I can.

No comments:

Post a Comment

Merci de votre commentaire. Celui-ci est en cours de modération.

Most seen