entrance

Flexible, lightweight and productive authentication for Plug, Phoenix and Ecto projects


License
MIT

Documentation

Entrance

Entrance

Version License Code Size

Flexible, lightweight and productive authentication for Plug, Phoenix and Ecto projects.

The primary goal of Entrance is to build an opinionated interface and easy to use API on top of flexible modules that can also be used directly.

When to choose Entrance:

  • You need a lightweight authentication framework that offers the basics in a productive way.
  • You have a project with Doorman authentication and want to upgrade it.

You can find more in-depth documentation here.

Table of contents

Installation

Add entrance to your dependencies in mix.exs.

def deps do
  [{:entrance, "~> 0.4.3"}]
end

Then add the configuration to your_app/config/config.exs

config :entrance,              
  repo: YourApp.Repo,
  security_module: Entrance.Auth.Bcrypt,
  user_module: YourApp.Accounts.User,
  default_authenticable_field: :email

Phoenix

First, generate a user schema with a hashed_password:string and session_secret:string field:

$ mix phx.gen.schema Accounts.User users email:string hashed_password:string session_secret:string

Run the migrations:

$ mix ecto.migrate

Next, use Entrance.Auth.Bcrypt in your new User module and add a virtual :password field. hash_password/1 is used in the changeset to hash our password and put it into the changeset as :hashed_password.

your_app/lib/your_app/accounts/user.ex

defmodule YourApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  import Entrance.Auth.Bcrypt, only: [hash_password: 1]

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true # Add this line
    field :hashed_password, :string
    field :session_secret, :string

    timestamps()
  end

  def create_changeset(user, attrs) do # Define a create_changeset function
    user
    |> cast(attrs, [:email, :password, :hashed_password, :session_secret]) # Dont forget to add :password here
    |> validate_required([:email, :password]) # And here
    |> hash_password # Add this line
  end
end

Finally, we can add our plug so we can have access to current_user on conn.assigns[:current_user]. 99% of the time that means adding the Entrance.Login.Session plug to your :browser pipeline:

your_app/lib/your_app_web/router.ex

  pipeline :browser do
    # ...

    plug Entrance.Login.Session
  end 

Creating Users

To create a user we can use the User.create_changeset/2 function we defined. Here we'll also add the session_secret to the user, which is only needed when creating an user or in case of compromised sessions. Example:

defmodule YourAppWeb.UserController do
  use YourAppWeb, :controller
  alias YourApp.Repo    
    
  alias Entrance.Auth.Secret
  alias YourApp.Accounts.User
    
  def new(conn, _params) do    
    changeset = User.create_changeset(%User{}, %{})
    conn |> render("new.html", changeset: changeset)
  end
    
  def create(conn, %{"user" => user_params}) do
    changeset =
      %User{}                  
      |> User.create_changeset(user_params)  
      |> Secret.put_session_secret()  
    
    case Repo.insert(changeset) do  
      {:ok, _user} ->
        conn |> redirect(to: "/")       
      {:error, changeset} ->
        conn |> render("new.html", changeset: changeset)
    end 
  end   
end  

If we want less boilerplate we can use Entrance.User.create/1 and Entrance.User.create_changeset/0 that does all this setup for us:

your_app/lib/your_app_web/controllers/user_controller.ex |$ mix entrance.gen.phx_user_controller

defmodule YourAppWeb.UserController do
  use YourAppWeb, :controller
    
  def new(conn, _params) do    
    conn |> render("new.html", changeset: Entrance.User.create_changeset)
  end
    
  def create(conn, %{"user" => user_params}) do
    case Entrance.User.create(user_params) do  
      {:ok, _user} ->
        conn |> redirect(to: "/")       
      {:error, changeset} ->
        conn |> render("new.html", changeset: changeset)
    end 
  end   
end  

We can also create users based in another schemas (not only the default configured in Mix.Config):

Entrance.User.create(Customer, customer_params)

And get their create_changesets too...

Entrance.User.create_changeset(Customer)

Logging in users

To login users we can use Entrance.auth and Entrance.Login.Session.login/2.

your_app/lib/your_app_web/controllers/session_controller.ex |$ mix entrance.gen.phx_session_controller

defmodule YourAppWeb.SessionController do
  use YourAppWeb, :controller
  import Entrance.Login.Session, only: [login: 2]

  def new(conn, _params) do
    render(conn, "new.html")
  end 

  def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
    if user = Entrance.auth(email, password) do
      conn
      |> login(user) # Sets :user_id and :session_secret on conn's session
      |> put_flash(:notice, "Successfully logged in")
      |> redirect(to: "/")
    else
      conn
      |> put_flash(:error, "No user found with the provided credentials")
      |> render("new.html")
    end 
  end 
end

Entrance have some other functions that might fit well too,

if you need...

More fields matching the user schema Entrance.auth_by:

Entrance.auth_by([email: email, admin: true], password)

More fields matching the same value, Entrance.auth_one:

Entrance.auth_one([:email, :nickname], my_nickname, password)

More fields matching the same value, and more fields matching the user schema Entrance.auth_one_by:

Entrance.auth_one_by({[:email, :nickname], my_nickname}, [admin: true], password)

Note: In this README example, we did not create the :admin or :nickname fields in Accounts.User schema

Read more about Entrance "auth functions" variations here.

Requiring Authentication

To require a user to be authenticated you can build a simple plug around Entrance.logged_in?/1.

your_app/lib/your_app_web/plugs/require_login.ex |$ mix entrance.gen.phx_require_login

defmodule YourAppWeb.Plugs.RequireLogin do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    if Entrance.logged_in?(conn) do
      conn
    else
      conn
      |> Phoenix.Controller.redirect(to: "/session/new")
      |> halt
    end
  end
end

An example in your_app/lib/your_app_web/router.ex:

pipeline :protected do
  plug YourAppWeb.Plugs.RequireLogin
end 

# ...

scope "/protected", YourAppWeb do
  pipe_through :browser
  pipe_through :protected

  get "/", PageController, :protected
end 

# ...

Logging out users

To logout users we can use Entrance.Login.Session.logout/1

your_app/lib/your_app_web/controllers/session_controller.ex |$ mix entrance.gen.phx_session_controller

defmodule YourAppWeb.SessionController do 
  use YourAppWeb, :controller  
  import Entrance.Login.Session, only: [login: 2, logout: 1] # Import logout
          
  def new(conn, _params) do
    render(conn, "new.html")   
  end 
      
  def create(conn, %{"session" => %{"email" => email, "password" => password}}) do
    if user = Entrance.auth(email, password) do
      conn
      |> login(user)           
      |> put_flash(:notice, "Successfully logged in")
      |> redirect(to: "/")     
    else  
      conn
      |> put_flash(:error, "No user found with the provided credentials")
      |> render("new.html")    
    end   
  end   
        
  # Add delete function to your sessions controller
  def delete(conn, _params) do 
    conn
    |> logout # Use logout function
    |> put_flash(:notice, "Successfully logged out")
    |> redirect(to: "/")       
  end
end 

Testing

You can easily test routes that require authentication following the example below:

your_app/test/your_app_web/controllers/page_controller_test.exs

defmodule YourAppWeb.PageControllerTest do
  use YourAppWeb.ConnCase

  import Entrance.Login.Session, only: [login: 2] # Add this line

  # Setup an logged_in_conn
  setup do
     # Create your test user
    {:ok, user} =
      Entrance.User.create(%{email: "test@test.com", password: "test"})

    opts =
      Plug.Session.init(
        store: :cookie,
        key: "test_key",
        encryption_salt: "test_encryption_salt",
        signing_salt: "test_signing_salt",
        log: false,
        encrypt: false
      )

    logged_in_conn =
      build_conn()
      |> Plug.Session.call(opts)
      |> fetch_session()
      |> login(user)

    %{logged_in_conn: logged_in_conn}
  end

  test "GET /protected", %{logged_in_conn: logged_in_conn} do
    response =
      logged_in_conn
      |> get("/protected")

    assert html_response(response, 200) # Yeah, it passes!
  end
end

Generating Modules

We can generate all the modules above with:

$ mix entrance.gen.phx_modules

This generator will add the following files to lib/:

  • a controller in lib/your_app_web/controllers/user_controller.ex
  • a view in lib/your_app_web/views/user_view.ex
  • a controller in lib/your_app_web/controllers/session_controller.ex
  • a view in lib/your_app_web/views/session_view.ex
  • a plug in lib/your_app_web/plugs/require_login.ex

And also a test file for each of this files.

We can set a different context if necessary:

$ mix entrance.gen.phx_modules --context Accounts

With "--context Accounts" it creates:

  • a controller in lib/your_app_web/controllers/accounts/user_controller.ex
  • a view in lib/your_app_web/views/accounts/user_view.ex
  • a controller in lib/your_app_web/controllers/accounts/session_controller.ex
  • a view in lib/your_app_web/views/accounts/session_view.ex
  • a plug in lib/your_app_web/plugs/accounts/require_login.ex

It's a nice start point for our app authentication.

Contribute

Entrance is not only for me, but for the Elixir community.

I'm totally open to new ideas. Fork, open issues and feel free to contribute with no bureaucracy. We only need to keep some patterns to maintain an organization:

branchs

your_branch_name

commits

[your_branch_name] Your commit

Credits

Entrance was built upon Doorman. Thanks to Blake Williams & Ashley Foster.

For the logo, thanks to Melissa Moreira.