Skip to content

Commit

Permalink
Merge pull request #716 from pow-auth/accept-mfas
Browse files Browse the repository at this point in the history
Accept MFAs along with anonymous functions
  • Loading branch information
danschultzer committed Sep 26, 2023
2 parents 6cc5911 + 2fd1d9c commit 6f80c37
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## v1.0.35 (TBA)

### Enhancements

* [`Pow.Ecto.Schema.Changeset`] Now handles MFA for `:password_hash_verify`
* [`Pow.Ecto.Schema.Changeset`] Now handles MFA for `:email_validator`

### Deprecations

* [`Pow.Ecto.Schema.Changeset`] Deprecated `:password_hash_methods` in favor of `:password_hash_verify`
Expand Down
38 changes: 21 additions & 17 deletions lib/pow/ecto/schema/changeset.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ defmodule Pow.Ecto.Schema.Changeset do
* `:password_min_length` - minimum password length, defaults to 8
* `:password_max_length` - maximum password length, defaults to 4096
* `:password_hash_verify` - the password hash and verify functions to use,
defaults to:
* `:password_hash_verify` - the password hash and verify anonymous
functions or MFAs, defaults to:
{&Pow.Ecto.Schema.Password.pbkdf2_hash/1,
&Pow.Ecto.Schema.Password.pbkdf2_verify/2}
* `:email_validator` - the email validation function, defaults to:
It may be anonymous functions of MFAs.
* `:email_validator` - the email validation anonymous function or
MFA, defaults to:
&Pow.Ecto.Schema.Changeset.validate_email/1
Expand Down Expand Up @@ -169,7 +172,7 @@ defmodule Pow.Ecto.Schema.Changeset do
validator = get_email_validator(config)

Changeset.validate_change(changeset, :email, {:email_format, validator}, fn :email, email ->
case validator.(email) do
case apply_function_or_mfa(validator, [email]) do
:ok -> []
:error -> [email: {"has invalid format", validation: :email_format}]
{:error, reason} -> [email: {"has invalid format", validation: :email_format, reason: reason}]
Expand Down Expand Up @@ -215,16 +218,12 @@ defmodule Pow.Ecto.Schema.Changeset do
"""
@spec verify_password(Ecto.Schema.t(), binary(), Config.t()) :: boolean()
def verify_password(%{password_hash: nil}, _password, config) do
config
|> get_password_hash_function()
|> apply([""])
apply_password_hash_function(config, [""])

false
end
def verify_password(%{password_hash: password_hash}, password, config) do
config
|> get_password_verify_function()
|> apply([password, password_hash])
apply_password_verify_function(config, [password, password_hash])
end

defp maybe_require_password(%{data: %{password_hash: nil}} = changeset) do
Expand Down Expand Up @@ -259,21 +258,26 @@ defmodule Pow.Ecto.Schema.Changeset do
defp maybe_validate_password_hash(changeset), do: changeset

defp hash_password(password, config) do
config
|> get_password_hash_function()
|> apply([password])
apply_password_hash_function(config, [password])
end

defp get_password_hash_function(config) do
defp apply_password_hash_function(config, args) do
{password_hash_function, _} = get_password_hash_functions(config)

password_hash_function
apply_function_or_mfa(password_hash_function, args)
end

defp apply_function_or_mfa(fun_or_mfa, apply_args) do
case fun_or_mfa do
fun when is_function(fun) -> apply(fun, apply_args)
{mod, fun, args} when is_list(args) -> apply(mod, fun, args ++ apply_args)
end
end

defp get_password_verify_function(config) do
defp apply_password_verify_function(config, args) do
{_, password_verify_function} = get_password_hash_functions(config)

password_verify_function
apply_function_or_mfa(password_verify_function, args)
end

defp get_password_hash_functions(config) do
Expand Down
54 changes: 51 additions & 3 deletions test/pow/ecto/schema/changeset_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ defmodule Pow.Ecto.Schema.ChangesetTest do
assert changeset.valid?
end

test "can validate with custom e-mail validator" do
test "can validate with custom anonymous function e-mail validator" do
config = [email_validator: &{:error, "custom message #{&1}"}]
changeset = Changeset.user_id_field_changeset(%User{}, @valid_params, config)

Expand All @@ -62,6 +62,31 @@ defmodule Pow.Ecto.Schema.ChangesetTest do
assert changeset.valid?
end

defmodule CustomEmailValidator do
def email_validate("[email protected]") do
:ok
end

def email_validate(email) do
{:error, "custom message #{email}"}
end
end

test "can validate with custom MFA function e-mail validator" do
config = [email_validator: {CustomEmailValidator, :email_validate, []}]
changeset = Changeset.user_id_field_changeset(%User{}, @valid_params, config)

refute changeset.valid?
assert changeset.errors[:email] == {"has invalid format", [validation: :email_format, reason: "custom message [email protected]"]}
assert changeset.validations[:email] == {:email_format, config[:email_validator]}

config = [email_validator: fn _email -> :ok end]
params = Map.put(@valid_params, "email", "[email protected]")
changeset = Changeset.user_id_field_changeset(%User{}, params, config)

assert changeset.valid?
end

test "uses case insensitive value for user id" do
changeset = User.changeset(%User{}, Map.put(@valid_params, "email", "[email protected]"))
assert changeset.valid?
Expand Down Expand Up @@ -210,12 +235,35 @@ defmodule Pow.Ecto.Schema.ChangesetTest do
end) =~ "passing `confirm_password` value to `Pow.Ecto.Schema.Changeset.confirm_password_changeset/3` has been deprecated, please use `password_confirmation` instead"
end

test "can use custom password hash functions" do
test "can use custom anonymous password hash functions" do
password_hash = &(&1 <> "123")
password_verify = &(&1 == &2 <> "123")
config = [password_hash_verify: {password_hash, password_verify}]
params = Map.put(@valid_params, "current_password", "secret")

changeset = Changeset.password_changeset(%User{}, @valid_params, config)
changeset = Changeset.password_changeset(%User{password: "secret123"}, params, config)

assert changeset.valid?
assert changeset.changes[:password_hash] == "secret1234123"
end

defmodule CustomPasswordHash do
def hash(password), do: password <> "123"
def verify(_password, hash), do: hash == "hashed"
end

test "can use custom MFA password hash functions" do
config =
[
password_hash_verify: {
{CustomPasswordHash, :hash, []},
{CustomPasswordHash, :verify, []}
}
]

params = Map.put(@valid_params, "current_password", "secret")

changeset = Changeset.password_changeset(%User{password_hash: "secret123"}, params, config)

assert changeset.valid?
assert changeset.changes[:password_hash] == "secret1234123"
Expand Down

0 comments on commit 6f80c37

Please sign in to comment.