AuthN
AuthN is a simple authentication library that allows you to
- authenticate a user against a database;
- maintain the user logged in;
- ensure private routes are accessed by logged in users only;
- use view helpers to access the logged in user from templates.
Authenticate the user
In your Schema representing the user accounts, specify which are the username and
password fields using the identifier_field/1
and password_field/1
macros:
use AuthN.Ecto.AuthNFields
schema "users" do
identifier_field :email
password_field :password_hash
field :name, :string
field :active, :boolean
end
DBAuthenticator.authenticate/3
allows you to authenticate the user against the
database. The first two arguments are the username and password, and the third
argument is a tuple containing your Repo module name and the user account Schema
struct. This function is typically called in the controller action handling login
submissions; the email and password being provided by the user through a login form.
alias AuthN.Authenticator.DBAuthenticator
case DBAuthenticator.authenticate(email, password, {MyApp.Repo, MyApp.Accounts.User}) do
{:ok, user} ->
conn
|> put_flash(:info, "Welcome back!")
|> redirect(to: Routes.profile_path(conn, :index))
{:error, :unknown_user} ->
conn
|> put_flash(:error, "No account found with that email address.")
|> render("new.html")
{:error, :wrong_password} ->
conn
|> put_flash(:error, "Invalid password")
|> render("new.html")
end
By default, the library will use the Argon2
hash function to verify passwords.
Argon2
is recommended over bcrypt
.See
argon2_elixir. You may change the
default password-hashing function through the :hashing_module
option, by passing
it a module name which implements the Comeonin
and Comeonin.PasswordHash
behaviours from the comeonin library, such
as bcrypt_elixir.
In most situations you do not want to reveal if the account exists in case of a failed authentication:
case DBAuthenticator.authenticate(email, password, {MyApp.Repo, MyApp.Accounts.User}) do
{:ok, user} ->
conn
|> put_flash(:info, "Welcome back!")
|> redirect(to: Routes.admin_question_path(conn, :index))
_ ->
conn
|> put_flash(:error, "Invalid username/password combination")
|> render("new.html")
end
Rather than calling DBAuthenticator.authenticate/3
from the controller, one can
instead call it from the context. Not only this is cleaner as the Repo should
preferably only be known by the context, but one can also add additional
application-specific authentication logic such as verifying if the user account
has been locked:
def authenticate(email, password) do
case DBAuthenticator.authenticate(email, password, {Repo, User}) do
{:ok, %{:active => true} = user} ->
{:ok, user}
{:ok, _} ->
{:error, :inactive_user}
error ->
error
end
end
Maintain the user logged in
After a successful authentication, the user ID should be stored into session in order to identify the logged user on subsequent requests.
SessionStorage.put_user_id/1
stores the user ID into the session.
alias AuthN.SessionStorage
case DBAuthenticator.authenticate(email, password, {MyApp.Repo, MyApp.Accounts.User}) do
{:ok, user} ->
conn
|> SessionStorage.put_user_id(user.id)
|> put_flash(:info, "Welcome back!")
|> redirect(to: Routes.profile_path(conn, :index))
# code
end
Note: By default, the session is stored in a stateless cookie. The library uses the functions from
Plug
to handle data in session, such asPlug.Conn.put_session/2
,Plug.Conn.get_session/2
,Plug.Conn.configure_session/1
, ...
Once the user ID has been stored into the session, the user can be retrieved by
the AuthN.Plugs.AssignCurrentUser
plug:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug AuthN.Plugs.AssignCurrentUser,
fetch_user: &MyApp.Accounts.get_user/1
end
The AssignCurrentUser
plug stores the logged in user into conn.assigns
under
the :current_user
key. The user is fetched by a user-defined function provided
to the plug, through the :fetch_user
option, and receiving the user ID as
argument.
Protecting routes against unauthenticated users
Authentication can be enforced for some routes. Create a module using (use
) the
AuthN.AuthenticationPlugMixin
module; then implement the callback
handle_authentication_error/2
receiving a Plug.Conn
struct and an atom
identifying the set of routes that require authentication:
defmodule MyAppWeb.Plugs.EnsureAuthenticated do
use AuthN.AuthenticationPlugMixin
import Plug.Conn
import Phoenix.Controller
def handle_authentication_error(conn, :admin_routes),
do: conn |> put_status(401) |> text("unauthenticated") |> halt()
end
You may then use the new plug into a pipeline and ensure that routes requiring authentication are accessed by logged in users only.
pipeline :ensure_admin_routes_authorized do
plug MyAppWeb.Plugs.EnsureAuthenticated,
resource: :admin_routes
end
scope "/admin", MyAppWeb, as: :admin do
pipe_through [:browser, :ensure_admin_routes_authorized]
# code
end
Installation
Add auth_n
for Elixir as a dependency in your mix.exs
file:
def deps do
[
{:auth_n, "~> 0.3.0"}
]
end
HexDocs
HexDocs documentation can be found at https://hexdocs.pm/auth_n.