ExPassword

This is a "port" of PHP password_* functions.

If you never used them, when you write an authentication system, you have to stick to the initial algorithm you chose.

This is not really acceptable, you should be able to change the hashing algorithm or its options at any time, without asking your users to reset their password. The goal here, when a change like that happens, is to ensure compatibility with previous algorithms and transparently update their hash when they successfuly log in.

Let's say you started an application with bcrypt (:expassword_bcrypt). But then:

* if you want and do it

But, wait, on my dev and/or test environments I can't compile NIFs. No problem: install PHP (have to be present in your PATH) and replace :expassword_bcrypt by :expassword_external_bcrypt and :expassword_argon2 by :expassword_external_argon2 (more details in their respective README.md)

Installation

If available in Hex, the package can be installed by adding expassword to your list of dependencies in mix.exs:

def deps do
  [
    {:expassword, "~> 0.1"},
    # with at least one the following
    {:expassword_argon2, "~> 0.1"},
    {:expassword_bcrypt, "~> 0.1"},
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/expassword.

Usage

defmodule MyAppWeb.SessionController do
  # ...

  @application :my_app
  def create(conn, %{"session" => %{"name" => login, "password" => password}}) do
    algorithm = Application.fetch_env!(@application, :password_algorithm)
    options = Application.fetch_env!(@application, :password_options)
    user = MyApp.Context.Users.get_user_by(name: login)
    if user do
      if ExPassword.verify?(password, user.encrypted_password) do
        if ExPassword.needs_rehash?(user.encrypted_password, options) do
          user
          |> Ecto.Changeset.change(encrypted_password: ExPassword.hash(algorithm, password, options))
          |> MyApp.Repo.update!()
        end

        conn
        |> put_flash(:info, "successfully authenticated")
        |> put_session(:user_id, user.id)
        |> redirect(to: "/")
      else
        conn
        |> put_flash(:error, "authentication failed")
        |> render("new.html")
      end
    else
      ExPassword.hash(algorithm, password, options) # for timing attacks

      conn
      |> put_flash(:error, "authentication failed")
      |> render("new.html")
    end
  end

  # same using the convenient ExPassword.verify_and_rehash_if_needed/6
  def create(conn, %{"session" => %{"name" => login, "password" => password}}) do
    algorithm = Application.fetch_env!(@application, :password_algorithm)
    options = Application.fetch_env!(@application, :password_options)
    user = MyApp.Context.Users.get_user_by(name: login)
    case ExPassword.verify_and_rehash_if_needed(user, password, algorithm, options) do
      {:ok, changes} ->
        if changes != [] do
          user
          |> Ecto.Changeset.change(changes)
          |> MyApp.Repo.update!()
        end
        conn
        |> put_flash(:info, "successfully authenticated")
        |> put_session(:user_id, user.id)
        |> redirect(to: "/")
      {:error, _reason} ->
        conn
        |> put_flash(:error, "authentication failed")
        |> render("new.html")
    end
  end
end

(this is more a pseudo code than real code)