Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to customize the hashed password field #31

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ databases) for the email column so that your email is case insensitive.
[citext]: https://www.postgresql.org/docs/9.1/static/citext.html

Next, use `Doorman.Auth.Bcrypt` in your new `User` module and add a virtual
`password` field. `hash_password/1` is used in the changeset to hash our
password and put it into the changeset as `hashed_password`.
`password` field. `hash_password/2` is used in the changeset to hash our
password and put it into the changeset as `hashed_password`. Also, you can
provide a map to `hash_password/2` in the form of `%{name: :field_name}` if
you want to customize which field to save the hashed password.

```elixir
defmodule MyApp.User do
use MyApp.Web, :model
import Doorman.Auth.Bcrypt, only: [hash_password: 1]
import Doorman.Auth.Bcrypt, only: [hash_password: 2]

schema "users" do
field :email, :string
Expand Down
31 changes: 29 additions & 2 deletions lib/auth/bcrypt.ex
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,42 @@ defmodule Doorman.Auth.Bcrypt do

@doc """
Takes a changeset and turns the virtual `password` field into a
`hashed_password` change on the changeset.
`hashed_password` change on the changeset. Also, it can take an
optional Map with the name of the field that maps to the database

## Example

```
defmodule User do
import Doorman.Auth.Bcrypt, only: [hash_password: 2]

import Ecto.Changeset

def create_changeset(struct, changes) do
struct
|> cast(changes, ~w(email password))
|> hash_password
end

def other_create_changeset(struct, changes) do
struct
|> cast(changes, ~w(email password))
|> hash_password(field_name: :my_password_hash)
end
end
```
"""
def hash_password(changeset) do
hash_password(changeset, field_name: :hashed_password)
end

def hash_password(changeset, %{"field_name" => field_name}) do
password = Changeset.get_change(changeset, :password)

if password do
hashed_password = Bcrypt.hashpwsalt(password)
changeset
|> Changeset.put_change(:hashed_password, hashed_password)
|> Changeset.put_change(field_name, hashed_password)
else
changeset
end
Expand Down
20 changes: 10 additions & 10 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
%{
"comeonin": {:hex, :comeonin, "2.4.0", "2dc7526b6352f7cf03de79c1e19b5154ac021da48b0c1790a12841ef5ca00340", [:mix, :make, :make], []},
"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], []},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, optional: true]}]},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"},
"plug": {:hex, :plug, "1.1.5", "de5645c18170415a72b18cc3d215c05321ddecac27a15acb923742156e98278b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, optional: true]}]},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], []},
"comeonin": {:hex, :comeonin, "2.4.0", "2dc7526b6352f7cf03de79c1e19b5154ac021da48b0c1790a12841ef5ca00340", [:make, :mix], [], "hexpm", "b326290a3143fdf4847a735f272ebd16d15216e97e968266a7b24125af4620be"},
"decimal": {:hex, :decimal, "1.1.2", "79a769d4657b2d537b51ef3c02d29ab7141d2b486b516c109642d453ee08e00c", [:mix], [], "hexpm", "7a6dfa1f4d389497acd7b807bf38c55022487c68b73d339d5114e3a691e006c5"},
"earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm", "b42a23e9bd92d65d16db2f75553982e58519054095356a418bb8320bbacb58b1"},
"ecto": {:hex, :ecto, "2.0.5", "7f4c79ac41ffba1a4c032b69d7045489f0069c256de606523c65d9f8188e502d", [:mix], [{:db_connection, "~> 1.0-rc.4", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.1.2 or ~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.7.7", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.12.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0-beta", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm", "da383b5932048c445527f993ea23c1e76895384b231759e34b6c23633f7f0fff"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "dc87f778d8260da0189a622f62790f6202af72f2f3dee6e78d91a18dd2fcd137"},
"makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d7152ff93f2eac07905f510dfa03397134345ba4673a00fbf7119bab98632940"},
"makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "4a36dd2d0d5c5f98d95b3f410d7071cd661d5af310472229dd0e92161f168a44"},
"nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm", "ebb595e19456a72786db6dcd370d320350cb624f0b6203fcc7e23161d49b0ffb"},
"plug": {:hex, :plug, "1.1.5", "de5645c18170415a72b18cc3d215c05321ddecac27a15acb923742156e98278b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}], "hexpm", "706871cb3d66c8c44cad4bceaa1f500eba34d5400450b9d63163d9dd4de88d3d"},
"poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm", "8f7168911120e13419e086e78d20e4d1a6776f1eee2411ac9f790af10813389f"},
}
28 changes: 28 additions & 0 deletions test/auth/bcrypt_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,22 @@ defmodule Doorman.Auth.BcryptTest do
end
end

defmodule CustomFieldFakeUser do
use Ecto.Schema
import Ecto.Changeset

schema "fake_users" do
field :password_hash
field :password, :string, virtual: true
end

def create_changeset(changes) do
%__MODULE__{}
|> cast(changes, ~w(password))
|> Doorman.Auth.Bcrypt.hash_password(field_name: :password_hash)
end
end

test "hash_password sets encrypted password on changeset when virtual field is present" do
changeset = FakeUser.create_changeset(%{password: "foobar"})

Expand All @@ -31,6 +47,18 @@ defmodule Doorman.Auth.BcryptTest do
refute changeset.changes[:hashed_password]
end

test "hash_password set encrypted password with a custom field name on changeset when virtual field is present" do
changeset = CustomFieldFakeUser.create_changeset(%{password: "pass123"})

assert changeset.changes[:password_hash]
end

test "hash_password does not set encrypted password with a custom field name on changeset when virtual field is present" do
changeset = CustomFieldFakeUser.create_changeset(%{})

refute changeset.changes[:password_hash]
end

test "authenticate returns true when password matches" do
password = "secure"
user = %FakeUser{hashed_password: Comeonin.Bcrypt.hashpwsalt(password)}
Expand Down