This module provides the glue for authenticating HTTP requests.
By using Authenticator, you'll get the following functions:
sign_in(conn, user)- Sign a user in.sign_out(conn)- Sign a user out.signed_in?(conn)- Check if a user is signed in.
You'll also get the following plugs:
plug :authenticate_session- Authenticate a user from the session.plug :authenticate_header- Authenticate a user from theAuthorizationheader.plug :ensure_authenticated- Make sure a user is signed in.plug :ensure_unauthenticated- Make sure a user is not signed in.
The package can be installed by adding authenticator to your list of dependencies in mix.exs:
def deps do [{:authenticator, "~> 1.0.0"}] endTo use Authenticator, you'll need to define the following functions:
tokenize(resource)- Serialize the user into a "token" that can be stored in the session.authenticate(resource)- Given a "token", locate the user.
Here's an example implementation of an authenticator:
# lib/my_app_web/authentication.ex defmodule MyAppWeb.Authentication do use Authenticator, fallback: MyAppWeb.FallbackController alias MyApp.Repo alias MyApp.Accounts.User @impl true def tokenize(user) do {:ok, to_string(user.id)} end @impl true def authenticate(user_id) do case Repo.get(User, user_id) do nil -> {:error, :unauthenticated} user -> {:ok, user} end end endIn your router, you'll define your plugs like so:
import MyAppWeb.Authenticator pipeline :browser do # snip... plug :authenticate_session end pipeline :authenticated do plug :ensure_authenticated end scope "/", MyAppWeb do pipe_through([:browser, :authenticated]) # declare protected routes here endThe controller where you're implementing login might look like this:
def create(conn, %{"email" => email, "password" => password}) do with {:ok, user} <- MyApp.Accounts.authenticate({email, password}) do conn |> MyAppWeb.Authentication.sign_in(user) |> redirect(to: "/") end end def destroy(conn, _params) do conn |> MyAppWeb.Authentication.sign_out() |> redirect(to: "/") endIn your router, you'll define your plugs like so:
import MyAppWeb.Authentication pipeline :browser do # snip... plug :authenticate_header end pipeline :authenticated do plug :ensure_authenticated end scope "/", MyAppWeb do pipe_through([:browser, :authenticated]) # declare protected routes here endThe controller where you're implementing login might look like this:
def create(conn, %{"email" => email, "password" => password}) do with {:ok, user} <- MyApp.Accounts.authenticate({email, password}), {:ok, token} <- MyAppWeb.Authenticator.tokenize(user) do conn |> MyAppWeb.Authentication.sign_in(user, session: false) |> json(%{token: token}) end end def destroy(conn, _params) do conn |> MyAppWeb.Authentication.sign_out(session: false) |> send_resp(204, "") endWhen an error occurs, the call/2 function of your fallback will be called. This is where you'd handle errors.
See the Phoenix docs for an example fallback controller.
defmodule MyAppWeb.FallbackController do use Phoenix.Controller import MyAppWeb.Router.Helpers # This would mean that the `:ensure_authenticated` plug failed. def call(conn, {:error, :unauthenticated}) do case get_format(conn) do "html" -> conn |> put_flash(:error, "You need to sign in to continue.") |> redirect(to: login_path(conn)) |> halt() "json" -> conn |> put_status(401) |> json(%{error: "You need to sign in to continue."}) |> halt() end end # This would mean that the `:ensure_unauthenticated` plug failed. def call(conn, {:error, :already_authenticated}) do conn |> put_flash(:error, "You are already signed in.") |> redirect(to: page_path(conn, :index)) |> halt() end endAuthenticator works very nicely with Authority and Authority.Ecto.
Here's an example authenticator:
defmodule MyAppWeb.Authentication do use Authenticator, fallback: MyAppWeb.FallbackController @impl true def tokenize(user) do with {:ok, token} <- MyApp.Accounts.tokenize(user) do {:ok, token.token} end end @impl true def authenticate(token) do MyApp.Accounts.authenticate(%MyApp.Accounts.Token{token: token}) end endNote: In the above example, we're serializing the user into a token. If you're using
Authority.Ecto, tokens are stored in the database. The benefit of using a token (as opposed to the user's ID), is that we can revoke specific sessions by deleting tokens from the database.