Curator
An Authentication Framework for Phoenix.
Curator is meant to mimic Devise, as such it provides several modules to accomplish authentication and various aspects of user lifecycle mangement (see modules below). It’s build with a module architecture that differs from existing Elixir Authentication solutions. Each curator module is a separate Hex package which can be combined to handle various authentication scenarios, passing coordination though a CuratorHooks module.
TODO: Curator uses Guardian for session management. However, a JWT approach may be inappropriate for some applications, and so API and Browser session management is configurable.
For an example, see the PhoenixCurator Application
Curator Modules
- Timeoutable: Session Timeout (after configurable inactivity).
- Recoverable: Reset the User Password.
- Lockable: Lock Account after configurbale count of invalid sign-ins.
- Database Authenticatable: Compare a password to a hashed password to support password based sign-in. Also provide a generator for creating a session page.
- Confirmable: Account email verification.
- Approvable: Require an approval step before user sign-in.
Installation
Add
curatorto your list of dependencies inmix.exs:def deps do [{:curator, "~> 0.1.0"}] endConfigure
config.exsconfig :curator, Curator, hooks_module: AuthApp.CuratorHooks, repo: AuthApp.Repo, user_schema: AuthApp.User, session_handler: Curator.SessionHandlers.GuardianRun the install command
mix curator.install User usersThis will generate:
- A user migration (priv/repo/migrations/
_create_user.exs)
defmodule <YourApp>.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :email, :string timestamps() end create unique_index(:users, [:email]) end end- A User Schema (web/models/user.ex)
defmodule <YourApp>.User do use <YourApp>.Web, :model # Use Curator Modules (as needed). # use CuratorDatabaseAuthenticatable.Schema schema "users" do field :email, :string # Add Curator Module fields (as needed). # curator_database_authenticatable_schema end def changeset(struct, params \\ %{}) do struct |> cast(params, [:email]) |> validate_required([:email]) |> validate_format(:email, ~r/@/) |> unique_constraint(:email) end end- An empty CuratorHooks Module (lib/
/curator_hooks.ex)
defmodule <YourApp>.CuratorHooks do use <YourApp>.Web, :controller use Curator.Hooks end- A session_helper (test/session_helper.ex)
The session_helper should be added to your test/support/conn_case.ex
using do quote do ... import <YourApp>.SessionHelper end end- An error_handler (web/controller/error_handler.ex)
You’ll want to customize the redirect paths. For instance, if you use a session
def unauthenticated(conn, %{reason: {:error, reason}}) do respond(conn, response_type(conn), 401, reason, session_path(conn, :new)) endTODO: This file is kinda messy and is built as a Guardian.ErrorHandler (which has a few extra callbacks)
- A user migration (priv/repo/migrations/
Add Plugs to the router
pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug Curator.Plug.LoadSession # Insert other Curator Plugs as necessary: # plug CuratorConfirmable.Plug plug Curator.Plug.EnsureResourceOrNoSession, handler: <YourApp>.ErrorHandler end pipeline :authenticated_browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers plug Curator.Plug.LoadSession # Insert other Curator Plugs as necessary: # plug CuratorConfirmable.Plug plug Curator.Plug.EnsureResourceAndSession, handler: <YourApp>.ErrorHandler endThe browser session will still cause a log out if the user is invalid, but will allow a non-session through. This allows plugs that alter the conn to still fire (timeoutable), but also allows new visitors to get to you splash page. Your prtected resources should all be piped through authenticated_browser pipeline. The addition curator pages are designed to be run through the new browser pipeline (unless otherwise indicated).
Testing. That’s important right?
With a protected route:
scope "/", PhoenixCurator do pipe_through :authenticated_browser # Use the default browser stack get "/secret", PageController, :secret endYou can use test authentication with the session_helper function sign_in_and_create_user:
defmodule PhoenixCurator.PageControllerTest do use PhoenixCurator.ConnCase alias Auth.User test "GET /", %{conn: conn} do conn = get conn, "/" assert html_response(conn, 200) =~ "Welcome to Phoenix!" end describe "testing authentication" do setup do conn = Phoenix.ConnTest.build_conn() |> conn_with_fetched_session {:ok, conn: conn} end test "visiting a secret page w/o a user", %{conn: conn} do conn = get conn, "/secret" assert Phoenix.Controller.get_flash(conn, :danger) == "Please Log In" # Customize this with as your redirect paths changes assert Phoenix.ConnTest.redirected_to(conn) == page_path(conn, :index) end test "sign_in_and_create_user", %{conn: conn} do {conn, _user} = sign_in_and_create_user(conn) conn = get conn, "/secret" # [Text Here](https://github.com/curator-ex/phoenix_curator/blob/master/web/templates/page/secret.html.eex) assert html_response(conn, 200) =~ "Sneaky, Sneaky, Sneaky..." end end endCurate.
Your authentication library is looking a bit spartan… Time to add to you collection.
You probably want a session page, so try out Database Authenticatable.
Curations
TODO: I’d love to support the idea of a curation, a collection of curator modules that can be installed with a single command. Unfortunatly, the generator code isn’t advanced enough to edit existing files so it’s a manual process for now.
Curator Hooks Module
1. Want your_verification to run on every request? Check out the pattern in [Confirmable](https://github.com/curator-ex/curator_confirmable). It requires an update to your curator_hooks module:
```elixir
def before_sign_in(user, type) do
with :ok <- your_verification(user) do
:ok
end
end
```
where your_verification returns :ok or {:error, 'message'}
And add a new Plug (in between your LoadResource and EnsureResource Plugs)
```elixir
def call(conn, opts) do
key = Map.get(opts, :key, Curator.default_key)
case Curator.PlugHelper.current_resource(conn, key) do
nil -> conn
{:error, _error} -> conn
current_resource ->
case your_verification(current_resource) do
:ok -> conn
{:error, error} -> Curator.PlugHelper.clear_current_resource_with_error(conn, error, key)
end
end
end
```
2. An example of utilizing Curator.Hooks.after_sign_in can be seen in the [Timeoutable](https://github.com/curator-ex/curator_timeoutable) Module.
3. An example of utilizing Curator.Hooks.after_failed_sign_in can be seen in the [Lockable](https://github.com/curator-ex/curator_lockable) Module.
4. Need More? There's a Curator.Hooks.after_extension callback which can be pattern matched for additional functionality, as seen in the [Approvable](https://github.com/curator-ex/curator_approvable) Module.
Session Management
Simple
TODO. This is not working yet.
The Simple Session Handler is a port of Guardian without JWT. Why? Some Blogs have raised objectsion that a JWT is too heavyweight (when you already have a session). Since we’re making Curator modular, we made the session configurable as well. We owe a huge debt to the Guardian team for making such a great pattern to follow.
Configure config.exs
config :curator, Curator,
hooks_module: AuthApp.CuratorHooks,
repo: AuthApp.Repo,
user_schema: AuthApp.User,
session_handler: Curator.SessionHandlers.Simple
Guardian
mix.exs
def deps do
[{:guardian, "~> 0.14"}]
end
config.exs
config :guardian, Guardian,
issuer: "AuthApp",
ttl: { 1, :days },
allowed_drift: 2000,
verify_issuer: true,
secret_key: <guardian secret key>,
serializer: Curator.UserSerializer
config :curator, Curator,
hooks_module: AuthApp.CuratorHooks,
repo: AuthApp.Repo,
user_schema: AuthApp.User,
session_handler: Curator.SessionHandlers.Guardian
Debt
Thanks go out to the Phoenix Team, the original rails project Devise, and the other elixir authentication solutions:
Guardian Coherence Sentinel Openmaize
Any decent ideas I credit to them, I was just acting as the curator.