Ash Way of Doing Password Hashing and Storage for Phoenix.

So mix phx.gen.auth will give you the some of the following in your_app/lib/your_app/accounts/user.ex:
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, MyApp.Repo)
|> unique_constraint(:email)
end

defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end

defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)

if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Argon2.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, MyApp.Repo)
|> unique_constraint(:email)
end

defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end

defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)

if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Argon2.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
This seems like the most straightforward way when starting a Phoenix app that has users. Phoenix generates a lot of other private functions for user auth, so for Ash do you create a new module to reference and use similar functions for validate/change blocks?
11 Replies
AlecStewart1#1125
So this is what I have so far that I believe is how it should be done in Ash:
actions do
defaults [:create :update, :read, :destroy] # will get rid if this line eventually
create :signup do
argument :password, :string do
allow_nil? false
sensitive? true
constraints [
allow_empty?: false,
min_length: 8,
max_length: 100,
trim?: true
]
end
argument :password_confirm, :string do
allow_nil? false
sensitive? true
constraints [
allow_empty?: false,
min_length: 8,
max_length: 100,
trim?: true
]
end
validate match(:password, ~r/[a-z]/)
validate match(:password, ~r/[A-Z]/)
validate match(:password, ~r/[!?@#$%^&*_0-9]/)
validate confirm(:password, :password_confirmation)
end
end
actions do
defaults [:create :update, :read, :destroy] # will get rid if this line eventually
create :signup do
argument :password, :string do
allow_nil? false
sensitive? true
constraints [
allow_empty?: false,
min_length: 8,
max_length: 100,
trim?: true
]
end
argument :password_confirm, :string do
allow_nil? false
sensitive? true
constraints [
allow_empty?: false,
min_length: 8,
max_length: 100,
trim?: true
]
end
validate match(:password, ~r/[a-z]/)
validate match(:password, ~r/[A-Z]/)
validate match(:password, ~r/[!?@#$%^&*_0-9]/)
validate confirm(:password, :password_confirmation)
end
end
kernel
kernel3y ago
there was a repo from a while ago (wont work out of the box with newest ash) https://github.com/ash-project/example_with_auth I'd recommend you use ash-authentication tbh https://github.com/ash-project/example_with_auth/blob/main/lib/example_with_auth/accounts/resources/user/changes/hash_password.ex
kernel
kernel3y ago
yeah this is the recommended way now
ZachDaniel
ZachDaniel3y ago
Yeah, ideally you'd use ash_authentication, but the validation code you mentioned above looks fine. Well, actually, you'd want only a single regex
validate match(:password, ~r/[a-z]/)
validate match(:password, ~r/[A-Z]/)
validate match(:password, ~r/[!?@#$%^&*_0-9]/)
validate match(:password, ~r/[a-z]/)
validate match(:password, ~r/[A-Z]/)
validate match(:password, ~r/[!?@#$%^&*_0-9]/)
that would fail on the first validation not matching. I.e A would not be allowed
AlecStewart1#1125
Makes sense. I'm terrible at regex so I'll have to remind myself how it would work for a password. I guess my one question is what exactly would I need to do in order to have ash_authentication use argon2 for hashing passwords?
ZachDaniel
ZachDaniel3y ago
You'd need to implement a hash_provider and configure it in the password strategy
hash_provider YourApp.Argon2Provider
hash_provider YourApp.Argon2Provider
AlecStewart1#1125
Ah okay, I think I'm following. What are the advantages to using the ash_authentication as opposed to doing it yourself? Mostly just flexibility?
ZachDaniel
ZachDaniel3y ago
There is also ash authentication phoenix Basically just not having to do it yourself. And we are adding features
AlecStewart1#1125
Oh okay, as an example, it's abstracting some of the CRUD actions you'd have to do for creating/updating a user password, right? Because I have the create :signup but I'd also need an update action for resetting the password etc. Oh okay, I looked at the ash_hq project and it's making more sense to me now.

Did you find this page helpful?