aidalgol

aidalgol

Password policies with with AshAuthentication

I just added a password check to my Ash+Phoenix application, and I thought others would find it useful.

This is starting from an application that is already set up with AshAuthenticationPhoenix to allow users to register with a password.

My User resource already had validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation on its register_with_password create action, so I looked at the source for that validation to see how AshAuthentication itself does validations on passwords, and came up with this.

defmodule MyApp.Validations.PasswordAllowed do
  @moduledoc """
  [Ash validation](https://hexdocs.pm/ash/validations.html) that enforces our
  application's password requirements.

  # Criteria
  - The password is not in the [Have I Been Pwned passwords database](https://haveibeenpwned.com/Passwords).
  """

  use Ash.Resource.Validation

  alias Ash.{
    Changeset,
    Error.Changes.InvalidArgument,
    Error.Framework.AssumptionFailed
  }

  alias AshAuthentication.Info

  @impl true
  def validate(changeset, opts, _context) do
    case Info.find_strategy(changeset, opts) do
      {:ok, %{password_field: _} = strategy} ->
        validate_password(changeset, strategy)

      # Allow non-password strategies.
      {:ok, _} ->
        :ok

      :error ->
        {:error,
         AssumptionFailed.exception(
           message: "Action does not correlate with an authentication strategy"
         )}
    end
  end

  @impl true
  def atomic(changeset, opts, context) do
    validate(changeset, opts, context)
  end

  defp validate_password(changeset, strategy) do
    password = Changeset.get_argument(changeset, strategy.password_field)

    # Skip check if password is nil
    if not is_nil(password) and MyApp.HaveIBeenPwnd.is_password_in_database?(password) do
      {:error,
       InvalidArgument.exception(
         field: strategy.password_field,
         message: "present in HaveIBeenPwnd database"
       )}
    else
      :ok
    end
  end
end

In this case, the only thing I want to enforce is that passwords from known data breaches are not allowed. If we wanted more password policies, say enforcing a minimum zxcvbn score, validate_password/2 would look something like this.

  defp validate_password(changeset, strategy) do
    password = Changeset.get_argument(changeset, strategy.password_field)

    # Skip check if password is nil
    cond do
      is_nil(password) ->
        :ok

      MyApp.HaveIBeenPwnd.is_password_in_database?(password) ->
        {:error,
         InvalidArgument.exception(
           field: strategy.password_field,
           message: "present in HaveIBeenPwnd database"
         )}

      MyApp.Zxcvbn.estimate(password).score < 2 ->
        {:error,
         InvalidArgument.exception(
           field: strategy.password_field,
           message: "zxcvbn score too low"
         )}

      _ ->
        :ok
    end
  end

Now for the HaveIBeenPwnd module. This API is well explained on the HIBP site, so I will not go into the this code piece by piece. My implementation uses Req Elixir HTTP client

defmodule MyApp.HaveIBeenPwnd do
  @moduledoc """
  Interface to the [Have I Been Pwned passwords API](https://haveibeenpwned.com/API/v3#PwnedPasswords).
  """
  @endpoint "https://api.pwnedpasswords.com/range/"
  @hash_algo :sha

  @doc """
  Checks whether `password` is in the HIBP breached passwords database.

  Raises an error on any failure so that any use of this fails closed.
  """
  def is_password_in_database?(password) do
    hash = :crypto.hash(@hash_algo, password) |> Base.encode16()
    hash_prefix = hash |> String.slice(0..4)

    # Raise an error because we want to fail closed if we cannot perform the
    # password check.
    response = Req.get!(@endpoint <> hash_prefix, http_errors: :raise)
    hash_suffixes = response.body |> String.split()

    hash_suffixes
    |> Stream.map(&String.split(&1, ":"))
    |> Enum.any?(fn [suffix, prevalence] -> hash_prefix <> suffix == hash and prevalence > 0 end)
  end
end

Now back to the User resource,

      # Validates that the password matches the confirmation
      validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

      if Application.compile_env(:myapp, :enforce_password_policy) do
        validate MyApp.Validations.PasswordAllowed
      end

(You will also want to set this on your password-reset action.) This adds the validation, but only if our application configuration option :enforce_password_policy was true at compile time, to allow us to enforce this only in a production deployment. If you want to enforce your password policies in all environments, you should be aware that the calls to the HaveIBeenPwnd API may make your tests fragile in a CI/CD environment.

And that’s it! When you try to register a new user account in your application with a password that’s in the HIBP database, AshAuthentication will reject the action with a clear error.

If this is for a Phoenix application, you may want to present a more friendly error message to your user. As a suggestion,

The password you have entered has appeared in a known data breach and is insecure.

That’s a bit wordy to put in an Ash validation error, so you may want to use the :transform_errors option on AshPhoenix.Form.for_action/3, which would also require implementing your own AshAuthenticationPhoenix sign-up component, but that’s something for another post.

Most Liked

zachdaniel

zachdaniel

Creator of Ash

If you make a custom exception for this you’ll likely have a better time.

defmodule MyApp.Errors.PwnedPassword do
  use Splode.Error, fields: [:field], class: :forbidden

  # shown internally if this error is raised
  def message(error) do
    "pwned password chosen"
  end
  # rendered in forms with this text
  defimpl AshPhoenix.FormData.Error do
    def to_form_error(error) do
       {error.field, "The password you have entered has appeared in a known data breach and is insecure.", []}
    end
  end
end

Then you can return {:error, MyApp.Errors.PwnedPassword.exception(field: :password)} from your validation.

One other note: you should add before_action?: true to the validation if you plan on putting it in forms otherwise it will check on every keystroke :smiley:

Where Next?

Popular in Discussions Top

blackode
Elixir Upgrading is so Simple in Ubuntu and It worked for me Ubuntu 16.04 git clone https://github.com/elixir-lang/elixir.git cd elixir...
New
Rustixir
Hi everyone, im working on find best language/framework/system for high concurrency, high performance and stable performance after wor...
New
Fl4m3Ph03n1x
Background A few days ago I was listening to The future of Elixir from Elixir Talks, with Dave Thomas (@pragdave ) and Brian Mitchell. I...
New
axelson
Decided against including more info in the title, but the gist is that Plataformatec sponsored projects will continue with the assets bei...
New
arcanemachine
https://nitter.net/josevalim/status/1744395345872683471 https://twitter.com/josevalim/status/1744395345872683471
New
AlexMcConnell
The reason that Rails is as popular as it is is because it’s very easy for relatively inexperienced developers to get a lot of work done....
588 19568 166
New
AstonJ
If a newbie asked you about Phoenix Contexts, how would you explain the basics to them? Feel free to be as concise or in-depth as you li...
New
AstonJ
Please see the new poll here: Which code editor or IDE do you use? (Poll) (2022 Edition) It’s been a while since we first asked this, I...
208 31142 143
New
marciol
Please, let me know if this kind of discussion already took place in another topic . Hi all, how do you consider if is better to build ...
New
rower687
Hi all, I’ve been reading a lot about the “let it crash” term and how supervising processes and the whole messaging passing make an elixi...
New

Other popular topics Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New

We're in Beta

About us Mission Statement