Packages
pow
0.1.0-alpha.6
1.0.39
1.0.38
1.0.37
1.0.36
1.0.35
1.0.34
1.0.33
1.0.32
1.0.31
1.0.30
1.0.29
1.0.28
1.0.27
1.0.26
1.0.25
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.1.0-rc.1
0.1.0-alpha.8
0.1.0-alpha.7
retired
0.1.0-alpha.6
0.1.0-alpha.5
0.1.0-alpha.4
0.1.0-alpha.3
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha
Robust user authentication solution
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/pow/ecto/schema/changeset.ex
defmodule Pow.Ecto.Schema.Changeset do
@moduledoc """
Handles changesets methods for Pow schema.
These methods should never be called directly, but instead the methods
build in macros in `Pow.Ecto.Schema` should be used. This is to ensure
that only compile time configuration is used.
## Configuration options
* `:password_min_length` - minimum password length, defaults to 10
* `:password_max_length` - maximum password length, defaults to 4096
* `:password_hash_methods` - the password hash and verify methods to use,
defaults to:
```elixir
{&Pow.Ecto.Schema.Password.pbkdf2_hash/1,
&Pow.Ecto.Schema.Password.pbkdf2_verify/2}
```
"""
alias Ecto.Changeset
alias Pow.{Config, Ecto.Schema, Ecto.Schema.Password}
@password_min_length 10
@password_max_length 4096
@doc """
Validates the user id field.
The user id field is always required. It will be treated as case insensitive,
and it's required to be unique. If the user id field is `:email`, the value
will be validated as an e-mail address too.
"""
@spec user_id_field_changeset(Ecto.Schema.t() | Changeset.t(), map(), Config.t()) :: Changeset.t()
def user_id_field_changeset(user_or_changeset, params, _config) do
user_id_field = case user_or_changeset do
%Changeset{data: %struct{}} -> struct.pow_user_id_field()
%struct{} -> struct.pow_user_id_field()
end
user_or_changeset
|> Changeset.cast(params, [user_id_field])
|> Changeset.update_change(user_id_field, &Schema.normalize_user_id_field_value/1)
|> maybe_validate_email_format(user_id_field)
|> Changeset.validate_required([user_id_field])
|> Changeset.unique_constraint(user_id_field)
end
@doc """
Validates the password field.
The `password` and `confirm_password` params is required to be equal. A
password hash is generated by using `:password_hash_methods` in the
configuration. The password is always required if the password hash is nil,
and it's required to be between `:password_min_length` to
`:password_max_length` characters long.
The password hash is only generated if the changeset is valid, but always
required.
"""
@spec password_changeset(Ecto.Schema.t() | Changeset.t(), map(), Config.t()) :: Changeset.t()
def password_changeset(user_or_changeset, params, config) do
user_or_changeset
|> Changeset.cast(params, [:password, :confirm_password])
|> maybe_require_password()
|> maybe_validate_password(config)
|> maybe_validate_confirm_password()
|> maybe_put_password_hash(config)
|> Changeset.validate_required([:password_hash])
end
@doc """
Validates the current password field.
It's only required to provide a current password if the `password_hash`
value exists in the data struct.
"""
@spec current_password_changeset(Ecto.Schema.t() | Changeset.t(), map(), Config.t()) :: Changeset.t()
def current_password_changeset(user_or_changeset, params, config) do
user_or_changeset
|> Changeset.cast(params, [:current_password])
|> maybe_validate_current_password(config)
end
defp maybe_validate_email_format(changeset, :email) do
Changeset.validate_format(changeset, :email, email_regexp())
end
defp maybe_validate_email_format(changeset, _), do: changeset
defp maybe_validate_current_password(%{data: %{password_hash: nil}} = changeset, _config),
do: changeset
defp maybe_validate_current_password(changeset, config) do
changeset = Changeset.validate_required(changeset, [:current_password])
case changeset.valid? do
true -> validate_current_password(changeset, config)
false -> changeset
end
end
defp validate_current_password(%{data: user, changes: %{current_password: password}} = changeset, config) do
user
|> verify_password(password, config)
|> case do
true -> changeset
_ -> Changeset.add_error(changeset, :current_password, "is invalid")
end
end
@doc """
Verifies a password in a struct.
The password will be verified by using the `:password_hash_methods` in the
configuration.
"""
@spec verify_password(Ecto.Schema.t(), binary(), Config.t()) :: boolean()
def verify_password(%{password_hash: password_hash}, password, config) do
config
|> password_verify_method()
|> apply([password, password_hash])
end
defp maybe_require_password(%{data: %{password_hash: nil}} = changeset) do
Changeset.validate_required(changeset, [:password])
end
defp maybe_require_password(changeset), do: changeset
defp maybe_validate_password(changeset, config) do
changeset
|> Changeset.get_change(:password)
|> case do
nil -> changeset
_ -> validate_password(changeset, config)
end
end
defp validate_password(changeset, config) do
password_min_length = Config.get(config, :password_min_length, @password_min_length)
password_max_length = Config.get(config, :password_max_length, @password_max_length)
Changeset.validate_length(changeset, :password, min: password_min_length, max: password_max_length)
end
defp maybe_validate_confirm_password(changeset) do
changeset
|> Changeset.get_change(:password)
|> case do
nil -> changeset
password -> validate_confirm_password(changeset, password)
end
end
defp validate_confirm_password(changeset, password) do
confirm_password = Changeset.get_change(changeset, :confirm_password)
case password do
^confirm_password -> changeset
_ -> Changeset.add_error(changeset, :confirm_password, "not same as password")
end
end
defp maybe_put_password_hash(%Changeset{valid?: true, changes: %{password: password}} = changeset, config) do
Changeset.put_change(changeset, :password_hash, hash_password(password, config))
end
defp maybe_put_password_hash(changeset, _config), do: changeset
defp hash_password(password, config) do
config
|> password_hash_method()
|> apply([password])
end
defp password_hash_method(config) do
{password_hash_method, _} = password_hash_methods(config)
password_hash_method
end
defp password_verify_method(config) do
{_, password_verify_method} = password_hash_methods(config)
password_verify_method
end
defp password_hash_methods(config) do
Config.get(config, :password_hash_methods, {&Password.pbkdf2_hash/1, &Password.pbkdf2_verify/2})
end
@rfc_5332_regexp_no_ip ~r<\A[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\z>
defp email_regexp, do: @rfc_5332_regexp_no_ip
end