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
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 ![]()
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








