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

  1. Add curator to your list of dependencies in mix.exs:

    def deps do
      [{:curator, "~> 0.1.0"}]
    end
  2. Configure config.exs

    config :curator, Curator,
      hooks_module: AuthApp.CuratorHooks,
      repo: AuthApp.Repo,
      user_schema: AuthApp.User,
      session_handler: Curator.SessionHandlers.Guardian
  3. Run the install command

      mix curator.install User users

    This will generate:

    1. 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
    1. 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
    1. An empty CuratorHooks Module (lib//curator_hooks.ex)
    defmodule <YourApp>.CuratorHooks do
      use <YourApp>.Web, :controller
      use Curator.Hooks
    end
    1. 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
    1. 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))
    end

    TODO: This file is kinda messy and is built as a Guardian.ErrorHandler (which has a few extra callbacks)

  4. 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
    end

    The 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).

  5. 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
    end

    You 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
    end
    
  6. Curate.

    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.

  7. 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

  1. mix.exs
  def deps do
    [{:guardian, "~> 0.14"}]
  end
  1. 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.