From dde36de22787a703d954ccb8ae2668cfedbe6af9 Mon Sep 17 00:00:00 2001 From: "Stephen M. Pallen" Date: Sat, 13 Oct 2018 13:00:19 -0400 Subject: [PATCH] Support configurable password hashing algorithms. * update comeonin to verson 4.1 * add installer option to set the password hashing algorithm * remove pre 20.0 erlang support (required for comeonin 4) * closes #299 * resolves #354 --- .travis.yml | 8 +--- README.md | 76 +++++++++++++++++++++++++++++- config/test.exs | 1 + lib/coherence/config.ex | 2 + lib/coherence/schema.ex | 6 +-- lib/mix/tasks/coh.install.ex | 60 ++++------------------- lib/mix/tasks/coherence.install.ex | 10 +--- mix.exs | 3 +- mix.lock | 3 +- 9 files changed, 98 insertions(+), 71 deletions(-) diff --git a/.travis.yml b/.travis.yml index d1591459..59feb931 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,17 +4,13 @@ before_script: - psql -c 'create database coherence_test;' -U postgres language: elixir elixir: + - 1.6 - 1.5 - 1.4 otp_release: - 20.0 - - 19.3 - - 18.3 matrix: - exclude: - - elixir: 1.3 - otp_release: 20.0 sudo: false notification: recipients: - - smpallen99@yahoo.com + - smpallen99@gmail.com diff --git a/README.md b/README.md index c5ae78ea..81523434 100644 --- a/README.md +++ b/README.md @@ -238,7 +238,8 @@ end {:unlock_token_expire_minutes, 5}, {:rememberable_cookie_expire_hours, 2*24}, {:forwarded_invitation_fields, [:email, :name]} -{:allow_silent_password_recovery_for_unknown_user, false} +{:allow_silent_password_recovery_for_unknown_user, false}, +{:password_hashing_alg, Comeonin.Bcrypt} ``` You can override this default configs. For example: you can add the following codes inside `config/config.exs` @@ -707,6 +708,79 @@ The list of controller actions are: * :session * :unlock +## Customizing Password Hashing Algorithm + +Coherence uses the `Bcrypt` algorithm by default for hashing passwords. However, with the update to Comeonin 4.0, you can now change the hashing algorithm. + +Comeonin currently supports the following 3 algorithms: + +* [Argon2](https://github.com/riverrun/argon2_elixir) +* [Bcrypt](https://github.com/riverrun/bcrypt_elixir) +* [Pbkdf2](https://github.com/riverrun/pbkdf2_elixir) + +### Change the Hashing Algorithm in an Existing Project + +To change the default in an existing project (to Argon2 for example), make the following 2 changes: + +* Edit your `config/config.exs` file add/change the following line: + +```elixir +# config/config.exs +config :coherence, + # ... + password_hashing_alg: Comeonin.Argon2, + # ... +``` + +* add the dependency to `mix.exs` + +```elixir + # mix.exs + defp deps do + [ + # ... + {:argon2_elixir, "~> 1.3"} + ] + end +``` + +### Change the Hashing Algorithm in an Existing Project + +To install Coherence in a new project with the `Pbkdf2` hashing algorithm (with the --full option for example): + +```bash +# mix coh.install --full --password-hashing-alg=Comeonin.Argon2 +``` + +and add the dependency + +```elixir + # mix.exs + defp deps do + [ + # ... + {:pbkdf2_elixir, "~> 0.12"} + ] + end +``` + +### Speed up Tests and Database Seeding of Users + +The default hashing algorithms are setup for production use. They are very slow by design which can cause very slow tests and database seeding in the dev and test environments. To speed this up, you can add the following to you `config/dev.exs` and/or `config/test.exs` configuration. + +However, *DON'T USE THESE SETTINGS IN PRODUCTION* + +```elixir +# config/test.exs +config :argon2_elixir, + t_cost: 1, + m_cost: 8 +config :bcrypt_elixir, log_rounds: 4 +config :pbkdf2_elixir, rounds: 1 +``` + +Note: Only configure the algorithm you have configured! + ## Accessing the Currently Logged In User During login, a current version of the user model is cashed in the credential store. During each authentication request, the user model is fetched from the credential store and placed in conn.assigns[:current_user] to avoid a database fetch on each request. diff --git a/config/test.exs b/config/test.exs index e072bdfb..de48de55 100644 --- a/config/test.exs +++ b/config/test.exs @@ -21,6 +21,7 @@ config :coherence, TestCoherence.Repo, config :coherence, user_schema: TestCoherence.User, + password_hashing_alg: Comeonin.Bcrypt, repo: TestCoherence.Repo, router: TestCoherenceWeb.Router, module: TestCoherence, diff --git a/lib/coherence/config.ex b/lib/coherence/config.ex index 242ecb31..5e25f944 100644 --- a/lib/coherence/config.ex +++ b/lib/coherence/config.ex @@ -40,6 +40,7 @@ defmodule Coherence.Config do * :module - the name of project module (`module: MyProject`) * :opts ([]) * :password_hash_field (:password_hash) - The field used to save the hashed password + * :password_hashing_alg (Comeonin.Bcrypt) - Password hashing algorithm to use. * :password_reset_permitted_attributes - List of allowed password reset atributes as stings, * :registration_permitted_attributes - List of allowed registration parameter attributes as strings * :repo: the module name of your Repo (`repo: MyProject.Repo`) @@ -114,6 +115,7 @@ defmodule Coherence.Config do :module, {:opts, []}, {:password_hash_field, :password_hash}, + {:password_hashing_alg, Comeonin.Bcrypt}, :password_reset_permitted_attributes, :registration_permitted_attributes, {:rememberable_cookie_expire_hours, 48}, diff --git a/lib/coherence/schema.ex b/lib/coherence/schema.ex index 9abb3c2b..dc995541 100644 --- a/lib/coherence/schema.ex +++ b/lib/coherence/schema.ex @@ -30,7 +30,7 @@ defmodule Coherence.Schema do The following functions are available when `authenticatable?/0` returns true: * `checkpw/2` - Validate password. - * `encrypt_password/1` - encrypted a password using `Comeonin.Bcrypt.hashpwsalt` + * `encrypt_password/1` - encrypted a password using `.hashpwsalt` * `validate_coherence/2` - run the coherence password validations. * `validate_password/2` - Used by `validate_coherence for password validation` @@ -292,7 +292,7 @@ defmodule Coherence.Schema do Keyword.get(unquote(opts), :authenticatable, true) do def checkpw(password, encrypted) do try do - Comeonin.Bcrypt.checkpw(password, encrypted) + apply(Config.password_hashing_alg(), :checkpw, [password, encrypted]) rescue _ -> false end @@ -301,7 +301,7 @@ defmodule Coherence.Schema do defoverridable checkpw: 2 def encrypt_password(password) do - Comeonin.Bcrypt.hashpwsalt(password) + apply(Config.password_hashing_alg(), :hashpwsalt, [password]) end def validate_coherence(changeset, params) do diff --git a/lib/mix/tasks/coh.install.ex b/lib/mix/tasks/coh.install.ex index ce97122d..6e82bd42 100644 --- a/lib/mix/tasks/coh.install.ex +++ b/lib/mix/tasks/coh.install.ex @@ -1,14 +1,4 @@ defmodule Mix.Tasks.Coh.Install do - use Mix.Task - - import Macro, only: [camelize: 1, underscore: 1] - import Mix.Generator - import Mix.Ecto - # import Coherence.Config, only: [use_binary_id?: 0] - import Coherence.Mix.Utils - - @shortdoc "Configure the Coherence Package" - @moduledoc """ Configure the Coherence User Model for your Phoenix application. Coherence is composed of a number of modules that can be enabled with this installer. @@ -109,6 +99,8 @@ defmodule Mix.Tasks.Coh.Install do A `--user-active-field` (false) add active field to user schema and disable logins when set to false. + A `--password-hashing-alg (Comeonin.Bcrypt) add a different password hashing algorithm + ## Disable Options * `--no-config` -- Don't append to your `config/config.exs` file. @@ -175,7 +167,8 @@ defmodule Mix.Tasks.Coh.Install do web_module: :string, binary_id: :boolean, layout: :boolean, - user_active_field: :boolean + user_active_field: :boolean, + password_hashing_alg: :string ] ++ Enum.map(@boolean_options, &{String.to_atom(&1), :boolean}) @switch_names Enum.map(@switches, &elem(&1, 0)) @@ -183,6 +176,7 @@ defmodule Mix.Tasks.Coh.Install do @new_user_migration_fields ["add :name, :string", "add :email, :string"] @new_user_constraints ["create unique_index(:users, [:email])"] + @spec run(command_line_args :: [binary]) :: any def run(args) do {opts, parsed, unknown} = OptionParser.parse(args, switches: @switches) @@ -292,6 +286,7 @@ defmodule Mix.Tasks.Coh.Install do module: #{config[:base]}, web_module: #{config[:web_base]}, router: #{config[:router]}, + password_hashing_alg: #{config[:password_hashing_alg]}, messages_backend: #{config[:web_base]}.Coherence.Messages,#{layout_field(config)} logged_out_url: "/",#{user_active_field(config)} registration_permitted_attributes: ["email","name","password","current_password","password_confirmation"], @@ -1184,6 +1179,7 @@ defmodule Mix.Tasks.Coh.Install do router = opts[:router] || "#{web_base}.Router" web_path = opts[:web_path] || web_path() web_module = web_base <> ".Coherence" + password_hashing_alg = opts[:password_hashing_alg] || "Comeonin.Bcrypt" binding = binding @@ -1227,7 +1223,8 @@ defmodule Mix.Tasks.Coh.Install do web_module: web_module, use_binary_id?: binding[:use_binary_id?], layout: opts[:layout] || false, - user_active_field?: binding[:user_active_field?] + user_active_field?: binding[:user_active_field?], + password_hashing_alg: password_hashing_alg ] |> Enum.into(opts_map) |> do_default_config(opts) @@ -1295,25 +1292,9 @@ defmodule Mix.Tasks.Coh.Install do defp parse_options(opts) do {opts_bin, opts} = Enum.reduce(opts, {[], []}, &option_reduce(&1, &2)) - # {:default, true}, {acc_bin, acc} -> - # {list_to_atoms(@default_options) ++ acc_bin, acc} - # {:full, true}, {acc_bin, acc} -> - # {list_to_atoms(@full_options) ++ acc_bin, acc} - # {:full_confirmable, true}, {acc_bin, acc} -> - # {list_to_atoms(@full_confirmable) ++ acc_bin, acc} - # {:full_invitable, true}, {acc_bin, acc} -> - # {list_to_atoms(@full_invitable) ++ acc_bin, acc} - # {:trackable_table, true}, {acc_bin, acc} -> - # {[:trackable_table | acc_bin] -- [:trackable], acc} - # {name, true}, {acc_bin, acc} when name in @all_options_atoms -> - # {[name | acc_bin], acc} - # {name, false}, {acc_bin, acc} when name in @all_options_atoms -> - # {acc_bin -- [name], acc} - # opt, {acc_bin, acc} -> - # {acc_bin, [opt | acc]} - # end opts_bin = Enum.uniq(opts_bin) + opts_names = Enum.map(opts, &elem(&1, 0)) with [] <- Enum.filter(opts_bin, &(not (&1 in @switch_names))), @@ -1326,27 +1307,6 @@ defmodule Mix.Tasks.Coh.Install do def all_options, do: @all_options_atoms - # def get_layout(opts) do - # get_layout_template opts[:layout] || false - # end - - # defp get_layout_template(true), do: {true, nil} - # defp get_layout_template(_) do - # case Path.wildcard web_path("templates/layout/app.html.*") do - # [] -> {true, nil} - # [first | _] -> get_layout_view(first) - # end - # end - - # defp get_layout_template(templ) do - # with {:ok, file} <- File.read(web_path("views/layout_view.ex")), - # [_, module] <- Regex.run(~r//, file) do - # {false, {module, Path.rootname(templ)}} - # else - # {true, nil} - # end - # end - def print_installed_options(_config) do ["mix coh.install"] |> list_config_options(Application.get_env(:coherence, :opts, [])) diff --git a/lib/mix/tasks/coherence.install.ex b/lib/mix/tasks/coherence.install.ex index 54d8cff1..cffa6ee6 100644 --- a/lib/mix/tasks/coherence.install.ex +++ b/lib/mix/tasks/coherence.install.ex @@ -1,13 +1,4 @@ defmodule Mix.Tasks.Coherence.Install do - use Mix.Task - - import Macro, only: [camelize: 1, underscore: 1] - import Mix.Generator - import Mix.Ecto - import Coherence.Mix.Utils - - @shortdoc "Configure the Coherence Package" - @moduledoc """ Configure the Coherence User Model for your Phoenix application. Coherence is composed of a number of modules that can be enabled with this installer. @@ -176,6 +167,7 @@ defmodule Mix.Tasks.Coherence.Install do @new_user_migration_fields ["add :name, :string", "add :email, :string"] @new_user_constraints ["create unique_index(:users, [:email])"] + @spec run(command_line_args :: [binary]) :: any def run(args) do {opts, parsed, unknown} = OptionParser.parse(args, switches: @switches) diff --git a/mix.exs b/mix.exs index c1d64ace..9c1175ab 100644 --- a/mix.exs +++ b/mix.exs @@ -48,7 +48,8 @@ defmodule Coherence.Mixfile do defp deps do [ {:ecto, "~> 2.0"}, - {:comeonin, "~> 3.0"}, + {:comeonin, "~> 4.0"}, + {:bcrypt_elixir, "~> 1.1"}, {:phoenix, "~> 1.3"}, {:phoenix_html, "~> 2.10"}, {:gettext, "~> 0.14"}, diff --git a/mix.lock b/mix.lock index f7c93493..852a963f 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,9 @@ %{ + "bcrypt_elixir": {:hex, :bcrypt_elixir, "1.1.1", "6b5560e47a02196ce5f0ab3f1d8265db79a23868c137e973b27afef928ed8006", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, "certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, - "comeonin": {:hex, :comeonin, "3.2.0", "cb10995a22aed6812667efb3856f548818c85d85394d8132bc116fbd6995c1ef", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "credo": {:hex, :credo, "0.10.0", "66234a95effaf9067edb19fc5d0cd5c6b461ad841baac42467afed96c78e5e9e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},