Skip to content
[Elixir] [BSD] A flexible authentication (and more) solution for Phoenix
Elixir HTML Ruby Shell
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.


Haytni is a configurable authentication system for Phoenix, inspired (and yet the word is weak) by Devise (should be almost compatible with it).


  • non-bloatware:
    • all logics are not located in controllers
    • minimize changes (upgrade)
  • easily customisable and extendable:
    • enable (or disable) any plugin
    • add your own plugin(s) to the stack


  • authenticable (Haytni.AuthenticablePlugin): handles hashing and storing an encrypted password in the database
  • registerable (Haytni.RegisterablePlugin): the elements to create a new account or edit its own account
  • rememberable (Haytni.RememberablePlugin): provides "persistent" authentification (the "remember me" feature)
  • confirmable (Haytni.ConfirmablePlugin): accounts have to be validated by email
  • recoverable (Haytni.RecoverablePlugin): recover for a forgotten password
  • lockable (Haytni.LockablePlugin): automatic lock an account after a number of failed attempts to sign in
  • trackable (Haytni.TrackablePlugin, only for PostgreSQL): register users's connections (IP + when)

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at


If available in Hex, the package can be installed by adding haytni to your list of dependencies in mix.exs:

def deps do
    {:haytni, "~> 0.0.2"},
    # ...

Run mix deps.get.

Configure Haytni your_app/config/config.exs

config :haytni,
  otp_app: :your_app,
  repo: YourApp.Repo,
  schema: YourApp.User,
  #mailer: YourApp.Mailer # see below

These are the mandatory options. See options of each plugin for full customizations.

Run mix haytni.install which has the following options (command arguments):

  • --table <table> (default: "users"): the name of your table (used to generate migrations)
  • --plugin Module1 --plugin Module2 ... --plugin ModuleN (default: value of config :haytni, plugins: [...]): the names of the (Elixir) modules/plugins to enable

Change your_app/lib/your_app_web/router.ex

defmodule YourAppWeb.Router do
  use YourAppWeb, :router
  require Haytni # <= add this line

  # ...

  pipeline :browser do
    # ...

    plug Haytni.CurrentUserPlug # <= add this line

  scope "/" do
    # ...

    Haytni.routes() # <= add this line

  # ...


Change your_app/lib/your_app/user.ex

defmodule YouApp.User do
  require Haytni # <= add this line

  # ...

  schema "..." do
    # ...

    Haytni.fields() # <= add this line

  # ...



For plugins which send emails (Confirmable, Lockable and Recoverable):

Create your_app/lib/mailer.ex as follows:

defmodule YourApp.Mailer do
  use Bamboo.Mailer, otp_app: :your_app

  def from, do: {"", ""}

Add to your_app/lib/your_app_web/router.ex

  if Mix.env() == :dev do

    forward "/sent_emails", Bamboo.SentEmailViewerPlug

Configure email sending in your_app/config/dev.exs:

config :your_app, YourApp.Mailer,
  adapter: Bamboo.LocalAdapter

config :haytni,
  mailer: YourApp.Mailer # <= add/change this line

For production (your_app/config/prod.exs), if you pass by your own SMTP server:

config :your_app, YourApp.Mailer,
  adapter: Bamboo.SMTPAdapter,
  server: "localhost", # the SMTP server is on the same host
  hostname: "",
  port: 25,
  tls: :never,
  no_mx_lookups: false,
  auth: :never

And add {:bamboo_smtp, "~> 1.7.0", only: :prod} to deps in your mix.exs file. See Bamboo's documentation for details and other methods to send emails

General configuration:

  • layout (default: false for none): the layout to apply to Haytni's templates
  • plugins (default: [Haytni.AuthenticablePlugin, Haytni.RegisterablePlugin, Haytni.RememberablePlugin, Haytni.ConfirmablePlugin, Haytni.LockablePlugin, Haytni.RecoverablePlugin]): a list of Haytni.Plugin modules to use

Warning: plugins order matters (in some case). Example: for correct handling of "automatic" authentification (find_user callback), Authenticable must appears before Rememberable in order to give precedence to the current session over the remember me cookie.




  • email (string)
  • encrypted_password (string)


  • authentication_keys (default: ~W[email]a): the key(s), in addition to the password, requested to login. You can redefine it to ~W[name]a, for example, to ask the username instead of its email address.
  • password hashing algorithm (default: bcrypt):
    • password_hash_fun (default: &Bcrypt.hash_pwd_salt/1): the function called to hash a password
    • password_check_fun (default: &Bcrypt.check_pass/2): function called to check if a password matches its hash

To use:

  • pbkdf2 add {:pbkdf2_elixir, "~> 1.0"} as deps to your mix.exs then set password_hash_fun to &Pbkdf2.hash_pwd_salt/1 and password_check_fun to &Pbkdf2.check_pass/2 in config/config.exs
  • argon2 add {:argon2_elixir, "~> 2.0"} as deps to your mix.exs then set password_hash_fun to &Argon2.hash_pwd_salt/1 and password_check_fun to &Argon2.check_pass/2 in config/config.exs


  • session_path (actions: new/create)


Change your_app/lib/your_app/user.ex

defmodule YourApp.User do
  # ...

  @attributes ~W[email password]a # add any field you'll may need
  # called when a user try to register himself
  def create_registration_changeset(%__MODULE__{} = struct, params) do
    |> cast(params, @attributes)
    |> validate_required(@attributes)
    # add any custom validation here
    |> Haytni.validate_create_registration()

  # called when a user try to edit its own account (logic is completely different from registration)
  def update_registration_changeset(%__MODULE__{} = struct, params) do
    |> cast(params, ~W[email password current_password]a)
    # /!\ email and password are not necessarily required here /!\
    # add any custom validation here
    |> Haytni.validate_update_registration()

  # ...


  • password_length (default: 6..128): define min and max password length as an Elixir Range
  • email_regexp (default: ~R/^[^@\s]+@[^@\s]+$/): the Regexp that an email at registration or profile edition needs to match
  • case_insensitive_keys (default: ~W[email]a): list of fields to automatically downcase on registration. May be unneeded depending on your database (eg: citext columns for PostgreSQL or columns with a collation suffixed by "_ci" for MySQL)
  • strip_whitespace_keys (default: ~W[email]a): list of fields to automatically strip from whitespaces
  • email_index_name (default: "users_email_index"): the name of the unique index/constraint on email field


  • registration_path (actions: new/create, edit/update)



  • remember_token (string, nullable, unique, default: NULL): the token to sign in automatically (NULL if the account doesn't use this function)
  • remember_created_at (datetime@utc, nullable, default: NULL): when the token was generated (also NULL if the account doesn't use this function)


  • remember_for (default: {2, :week}): the period of validity of the token/which the user won't be asked for credentials
  • remember_salt (default: ""): the salt to (de)cipher the token stored in the (signed) cookie
  • remember_token_length (default: 16): the length of the token (before being ciphered)
  • remember_cookie_name (default: "remember_token"): the name of the cookie holding the token for automatic sign in
  • remember_cookie_options (default: [http_only: true, extra: "Samesite=Strict"]): to set custom options of the cookie (options are: domain, max_age, path, http_only, secure and extra, see documentation of Plug.Conn.put_resp_cookie/4)

Routes: none



  • confirmed_at (datetime@utc, nullable, default: NULL): when the account was confirmed else NULL
  • confirmation_sent_at (datetime@utc): when the confirmation was sent
  • confirmation_token (string, nullable, unique, default: NULL): the token to be confirmed if any pending confirmation (else NULL)
  • unconfirmed_email (string, nullable, default: NULL): on email change the new email is stored here until its confirmation


  • reconfirmable (default: true): if true, on an email change, the user has to confirm its new address
  • confirmation_keys (default: ~W[email]a): the key(s) to be matched before sending a new confirmation
  • confirm_within (default: {3, :day}): delay after which confirmation token is considered as expired (ie the user has to ask for a new one)


  • confirmation_path (actions: show, new/create)



  • reset_password_token (string, nullable, unique, default: NULL): the unique token to reinitialize the password (NULL if none)
  • reset_password_sent_at (datetime@utc, nullable, default: NULL): when the reinitialization token was generated (also NULL if there is no pending request)


  • reset_token_length (default: 32): the length of the generated token
  • reset_password_within (default: {6, :hour}): the delay before the token expires
  • reset_password_keys (default: ~W[email]a): the field(s) to be matched to send a reinitialization token


  • password_path (actions: new/create, edit/update)



  • failed_attempts (integer, default: 0): the current count of successive failures to login
  • locked_at (datetime@utc, nullable, default: NULL): when the account was locked (NULL while the account is not locked)
  • unlock_token (string, nullable, unique, default: NULL): the token send to the user to unlock its account


  • maximum_attempts (default: 20): the amount of successive attempts to login before locking the corresponding account
  • unlock_token_length (default: 32): the length of the generated token
  • unlock_keys (default: ~W[email]a): the field(s) to match to accept the unlock request


  • unlock_path (actions: new/create, show)

Trackable (PostgreSQL only)


  • current_sign_in_at (datetime@utc, nullable, default: NULL): the date/time of the last login of a user (NULL if he never used its account)
  • last_sign_in_at (datetime@utc, nullable, default: NULL): the date/time of its previous login (NULL if the user signs in less than twice)

Configuration: none

Routes: none

Quick recap

Functions you have to implement:

  • for Registerable: YourApp.User.create_registration_changeset/2 and YourApp.User.update_registration_changeset/2
  • for sending emails (plugins Confirmable, Lockable and Recoverable): YourApp.Mailer.from/0
You can’t perform that action at this time.