Production checklist

Before you deploy your Pow enabled app to production, you should take a look at the following checklist to ensure that your app is ready.

The list is not exhaustive, and you should take any appropriate additional steps for your particular setup.

REQUIRED: Use a persistent cache store

By default the Pow.Store.Backend.EtsCache will be used as the cache backend. In production, this would mean that all session data will be lost between deploys or server restarts. Furthermore, in clusters, the sessions will not be shared between nodes.

You should use a persistent (and possibly distributed) cache store like the Pow.Store.Backend.MnesiaCache.

To enable the Mnesia cache you should add it to your application.ex supervisor:

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyAppWeb.Endpoint,
      Pow.Store.Backend.MnesiaCache
      # # Or in a distributed system:
      # {Pow.Store.Backend.MnesiaCache, extra_db_nodes: {Node, :list, []}},
      # Pow.Store.Backend.MnesiaCache.Unsplit # Recover from netsplit
    ]

    opts = [strategy: :one_for_one, name: MyAppWeb.Supervisor]
    Supervisor.start_link(children, opts)
  end

  # ...
end

Update the config with cache_store_backend: Pow.Store.Backend.MnesiaCache.

Mnesia will store the database files in the directory ./Mnesia.NODE in the current working directory where NODE is the node name. By default, this is ./Mnesia.nonode@nohost. You may wish to change the location to a shared directory so you can roll deploys:

config :mnesia, :dir, '/path/to/dir'

:mnesia should be added to :extra_applications in mix.exs for it to be included in releases.

OPTIONAL: Validate that strong passwords are used

NIST 800-63b recommends that you reject passwords that are commonly-used, expected, or compromised. The guidelines explicitly mention the following methods to ensure strong passwords are used:

  • Passwords obtained from previous breach corpuses.
  • Dictionary words.
  • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
  • Context-specific words, such as the name of the service, the username, and derivatives thereof.

You can read how to handle password breach lookup and other NIST based validation rules on the powauth.com website.

OPTIONAL: Use an appropriate password hash method

By default PBKDF2-SHA512 with 100,000 iterations is used for password hashing. This is what's recommended by NIST 800-63b. If you are allowed to use other password hashing algorithms, then Argon2id is considered a safer option.

You can easily change the password hashing method in Pow. Here's how you can use comeonin with Argon2:

defmodule MyApp.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema,
    password_hash_methods: {&Argon2.hash_pwd_salt/1,
                            &Argon2.verify_pass/2}

  # ...
end

OPTIONAL: Rate limit authentication attempts

You should rate limit authentication attempts according to NIST 800-63b. Pow doesn't include a rate limiter since this is better dealt with at the proxy or gateway side rather than the application side. The minimum requirement would be to rate limit to a maximum of 100 failed authentication attempts per IP.

You may also wish to lock accounts that has had too many failed authentication attempts, or require a CAPTCHA to be solved before allowing new attempts.

OPTIONAL: Rate limit e-mail delivery

There are no rate limits for any e-mails sent out with Pow, including PowEmailConfirmation, PowInvitation and PowResetPassword extensions. If you use a transactional e-mail service you have to make careful considerations to prevent resource usage attacks.

Rate limitation should either be handled at the service, or you may be able to set up rate limitation in the Pow mailer. For the latter, here's a simple example using Hammer:

defmodule MyAppWeb.Pow.Mailer do
  use Pow.Phoenix.Mailer

  # ....

  require Logger

  @impl true
  def process(email) do
    case check_rate(email) do
      {:allow, _count} -> deliver(email)
      {:deny, _count}  -> Logger.warn("Mailer backend failed due to rate limitation: #{inspect(email)}")
    end
  end

  defp check_rate(%{to: email}) do
    Hammer.check_rate_inc("email:#{email}", :timer.minutes(1), 2, 1)
  end
end

In the above, the e-mail delivery will be limited to two e-mails per minute for a single recipient, but you can use different criteria, e.g. limit for e-mails that have the same recipient and subject. It's strongly recommended to add tests where appropriate to ensure abuse is not possible.