README.md

# Curator

An Authentication Framework for Phoenix.

Curator is meant to mimic [Devise](https://github.com/plataformatec/devise), as such it provides [several modules](#curator-modules) to accomplish authentication and various aspects of user lifecycle mangement. It's build with a modular architecture that differs from existing Elixir Authentication solutions. Each curator module can be combined to handle various authentication scenarios, passing coordination through a curator module](#curator-module). Under the hood, this uses [Guardian](https://github.com/ueberauth/guardian) for session management.

For an example, see the [PhoenixCurator Application](https://github.com/curator-ex/phoenix_curator)

## Curator Modules

* [Ueberauth](#ueberauth): Ueberauth Integration.
* [Timeoutable](#timeoutable): Session Timeout (after configurable inactivity).

(TODO)

* [Registerable](#registerable): A Generator to support user registration.
* [Database Authenticatable](#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](#confirmable): Account email verification.
* [Recoverable](#recoverable): Reset the User Password.
* [Lockable](#lockable): Lock Account after configurbale count of invalid sign-ins.
* [Approvable](#approvable): Require an approval step before user sign-in.

## Installation

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

    ```elixir
    def deps do
      [{:curator, "~> 0.2.2"}]
    end
    ```

2. Run the install command

    ```
    mix curator.install
    ```

    This will generate:

    1. A User context, migration, and schema (in the Ecto application if an umbrella)

        * A user migration (`priv/repo/migrations/<timestamp>_create_users.exs`)
        * A user schema (`<my_app>/lib/<my_app>/auth/user.ex`)
        * A user context (`<my_app>/lib/<my_app>/auth/auth.ex`)

    2. An empty Curator module (`<my_app_web>/lib/<my_app_web>/auth/curator.ex`)

    3. A Guardian Configuration

        * A Guardian module (`<my_app_web>/lib/<my_app_web>/auth/guardian.ex`)
        * An error handler  (`<my_app_web>/lib/<my_app_web>/controllers/auth/error_handler.ex`)

    4. A view helper (`<my_app_web>/lib/<my_app_web>/views/auth/curator_helper.ex`)

    5. A Session Controller

        * controller (`<my_app_web>/lib/<my_app_web>/controllers/auth/session_controller.ex`)
        * view helper (`<my_app_web>/lib/<my_app_web>/views/auth/curator_helper.ex`)
        * new template (`<my_app_web>/lib/<my_app_web>/templates/auth/session/new.html.eex`). Note: this is just a placeholder that you'll want to update when you decide on a sign-in strategy.

3. The generators aren't perfect (TODO), so finish the installation
    
    1. Update your router (`<my_app_web>/lib/<my_app_web>/router.ex`)
    
        ```elixir
        require Curator.Router
        
        pipeline :browser do
          ...
          plug <MyWebApp>.Auth.Curator.UnauthenticatedPipeline
        end
        
        pipeline :authenticated_browser do
          ... (copy the code from browser)
          plug <MyWebApp>.Auth.Curator.AuthenticatedPipeline
        end
        
        scope "/", <MyWebApp> do
          pipe_through :browser
            
          ...
          Insert your unprotected routes here
          ...
        
          Curator.Router.mount_unauthenticated_routes(<MyWebApp>.Auth.Curator)
        end
        
        scope "/", <MyWebApp> do
          pipe_through :authenticated_browser
          
          ...
          Insert your urotected routes here
          ...
        
          Curator.Router.mount_authenticated_routes(<MyWebApp>.Auth.Curator)
        end
        ```
    
    2. Add the view_helper to your web module (`<my_app_web>/lib/<my_app_web>.ex`)
    
        ```elixir
        def view do
          quote do
            ...
        
            import <MyAppWeb>.Auth.CuratorHelper
          end
        end
        ```
        
        This allows you to call `current_user(@conn)` in your templates
    
    3. [Configure Guardian](https://github.com/ueberauth/guardian#installation) in `config.exs`
    
        ```elixir
        config :<my_app_web>, <MyAppWeb>.Auth.Guardian,
          issuer: "<my_app_web>",
          secret_key: "Secret key. You can use `mix guardian.gen.secret` to get one"
        ```
        
    4. Add to your Auth Context (`<my_app>/lib/<my_app>/auth/auth.ex`)
        
        ```elixir
          def get_user(id), do: Repo.get(User, id)
        ```
    
4. Add a signout link to your layout
    
    ```elixir
    <%= if current_user(@conn) do %>
      <%= link "Sign Out", to: session_path(@conn, :delete), method: :delete %>
    <% end %>
    ```
    
5. Testing

    Update `conn_case.ex`:

    ```elixir
    setup tags do
      :ok = Ecto.Adapters.SQL.Sandbox.checkout(<MyApp>.Repo)
      unless tags[:async] do
        Ecto.Adapters.SQL.Sandbox.mode(<MyApp>.Repo, {:shared, self()})
      end
    
      # Create w/ ExMachina (or your preferred method)
      # Note: As you add additional modules, make sure this user is valid for them too.
      auth_user = <MyApp>.Factory.insert(:auth_user)
    
      {:ok, token, claims} = <MyAppWeb>.Auth.Guardian.encode_and_sign(auth_user)
    
      conn = Phoenix.ConnTest.build_conn()
    
      auth_conn = conn
      |> Plug.Test.init_test_session(%{
        guardian_default_token: token,
        guardian_default_timeoutable: Curator.Time.timestamp(),
      })
    
      {:ok, unauth_conn: conn, auth_user: auth_user, conn: auth_conn}
    end
    ```

    Note: This uses `conn` as an authenticated connection, so existing tests won't need to be updated.

    To test, I created some special routes:

    ```elixir
    scope "/", <MyAppWeb> do
      pipe_through :browser
    
      if Mix.env == :test do
        get "/insecure", PageController, :insecure
      end
    
      Curator.Router.mount_unauthenticated_routes(<MyAppWeb>.Auth.Curator)
    end
    
    scope "/", <MyAppWeb> do
      pipe_through :authenticated_browser
    
      if Mix.env == :test do
        get "/secure", PageController, :secure
      end
    
      Curator.Router.mount_authenticated_routes(<MyAppWeb>.Auth.Curator)
    end
    ```

    Update the `page_controller.ex`:

    ```elixir
    defmodule <MyAppWeb>.PageController do
      use <MyAppWeb>, :controller
    
      def secure(conn, _params) do
        text conn, "!!!SECURE!!!"
      end
    
      def insecure(conn, _params) do
        text conn, "INSECURE"
      end
    end
    ```

    And wrote tests in `page_controller_test.exs`:

    ```
    defmodule <MyAppWeb>.PageControllerTest do
      use <MyAppWeb>.ConnCase
    
      test "GET /secure (Unauthenticated)", %{unauth_conn: conn} do
        conn = get conn, "/secure"
        assert redirected_to(conn) == session_path(conn, :new)
        assert get_flash(conn, :error) == "Please Sign In"
      end
        
      test "GET /secure (Authenticated)", %{conn: conn} do
        conn = get conn, "/secure"
        assert text_response(conn, 200) == "!!!SECURE!!!"
      end
        
      test "GET /secure (Authenticated - User Delete)", %{conn: conn, auth_user: user} do
        <MyApp>.Auth.delete_user(user)
        conn = get conn, "/secure"
        assert redirected_to(conn) == session_path(conn, :new)
        assert get_flash(conn, :error) == "Please Sign In"
      end
        
      test "GET /insecure (Unauthenticated)", %{unauth_conn: conn} do
        conn = get conn, "/insecure"
        assert text_response(conn, 200) == "INSECURE"
      end
        
      test "GET /insecure (Authenticated)", %{conn: conn} do
        conn = get conn, "/insecure"
        assert text_response(conn, 200) == "INSECURE"
      end
        
      test "GET /insecure (Authenticated - User Delete)", %{conn: conn, auth_user: user} do
        <MyApp>.Auth.delete_user(user)
        conn = get conn, "/insecure"
        assert redirected_to(conn) == session_path(conn, :new)
        assert get_flash(conn, :error) == "Please Sign In"
      end
    end
    ```

    These examples can be extended as additional modules are integrated (ex. using Confirmable with a user that hasn't been confirmed).

6. Curate.

    Your authentication library is looking a bit spartan... Time to add to you collection.
    
    Currently only an oauth workflow is supported, so start with [Ueberauth](#ueberauth)

## Module Documentation

### Ueberauth

#### Description
Ueberauth Integration

#### Installation

1. Run the install command

    ```
    mix curator.ueberauth.install
    ```

2. Add to the curator modules (`<my_app_web>/lib/<my_app_web>/auth/curator.ex`)

    ```elixir
    use Curator, otp_app: :my_app_web,
      modules: [
        <MyAppWeb>.Auth.Ueberauth,
      ]
    ```

3. Install Ueberauth and the desired [strategies](https://github.com/ueberauth/ueberauth#configuring-providers). For example, to add google oauth:
    
    1. Update `mix.exs`
    
        ```elixir
        defp deps do
          [
            {:ueberauth, "~> 0.4"},
            {:ueberauth_google, "~> 0.7"},
          ]
        end
        ```
    
        NOTE: If you're using an umbrella app you'll also need to add ueberauth to your ecto applications `mix.exs`.
    
    2. Update `config.exs`
    
        ```elixir
        config :ueberauth, Ueberauth,
          providers: [
            google: {Ueberauth.Strategy.Google, []}
          ]
        
        config :ueberauth, Ueberauth.Strategy.Google.OAuth,
          client_id: System.get_env("GOOGLE_CLIENT_ID"),
          client_secret: System.get_env("GOOGLE_CLIENT_SECRET")
        ```

    3. Put some links to the providers (`<my_app_web>/lib/<my_app_web>/templates/auth/session/new.html.eex`)
    
        ```elixir
        <%= link "Google", to: ueberauth_path(@conn, :request, "google"), class: "btn btn-default" %>
        ```

### Timeoutable

#### Description
Session Timeout (after configurable inactivity)

#### Installation

1. Run the install command

    ```
    mix curator.timeoutable.install
    ```

2. Add to the curator modules (`<my_app_web>/lib/<my_app_web>/auth/curator.ex`)

    ```elixir
    use Curator, otp_app: :<my_app_web>,
      modules: [
       <MyAppWeb>.Auth.Timeoutable,
      ]
    ```

3. Add to the curator plugs

    ```elixir
    defmodule <MyAppWeb>.Auth.Curator.UnauthenticatedPipeline do
      ...
      plug Curator.Timeoutable.Plug, timeoutable_module: <MyAppWeb>.Auth.Timeoutable
    end
    
    defmodule <MyAppWeb>.Auth.Curator.AuthenticatedPipeline do
      ...
      plug Curator.Timeoutable.Plug, timeoutable_module: <MyAppWeb>.Auth.Timeoutable
    end
    ```

4. (optional) Configure Timeoutable (`<my_app_web>/lib/<my_app_web>/auth/timeoutable.ex`)

    ```elixir
    use Curator.Timeoutable, otp_app: :<my_app_web>,
      timeout_in: 1800
    ```

5. Update tests (`<my_app_web>/test/support/conn_case.ex`)

    ```elixir
    auth_conn = conn
    |> Plug.Test.init_test_session(%{
      guardian_default_token: token,
      guardian_default_timeoutable: Curator.Time.timestamp(),
    })
    ```

    This session key usually is set as part of the after_sign_in extension.

6. (optional) Update the ErrorHandler (`<my_app_web>/lib/<my_app_web>/controllers/auth/error_handler.ex`)

    ```elixir
    defp translate_error({:timeoutable, :timeout}), do: "You have been signed out due to inactivity"
    ```

### Registerable (TODO)

### Database Authenticatable (TODO)

### Confirmable (TODO)

### Recoverable (TODO)

### Lockable (TODO)

### Approvable (TODO)

# Extending Curator (TODO)

# Debt
Thanks go out to the [Phoenix Team](https://github.com/phoenixframework/phoenix), the original rails gem [Devise](https://github.com/plataformatec/devise), [Guardian](https://github.com/ueberauth/guardian), and the other elixir authentication solutions:

* [Coherence](https://github.com/smpallen99/coherence)
* [Sentinel](https://github.com/britton-jb/sentinel)
* [Openmaize](https://github.com/riverrun/openmaize)

Any decent ideas I credit to them, I was just acting as the curator.