![Entrance](https://www.truehenrique.com/images/entrance_logo.png)
# Entrance
![Version](https://img.shields.io/hexpm/v/entrance?style=flat-square)
![License](https://img.shields.io/github/license/henriquefernandez/entrance?style=flat-square)
![Code Size](https://img.shields.io/github/languages/code-size/henriquefernandez/entrance?style=flat-square)
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](https://github.com/BlakeWilliams/doorman) authentication and want to upgrade it.
You can find more in-depth [documentation here](https://hexdocs.pm/entrance/Entrance.html#content).
## Table of contents
- [Installation](#installation)
- [Phoenix](#phoenix)
- [Creating users](#creating-users)
- [Logging in users](#logging-in-users)
- [Requiring Authentication](#requiring-authentication)
- [Logging out users](#logging-out-users)
- [Testing](#testing)
- [Generating Modules](#generating-modules)
- [Contribute](#contribute)
- [Credits](#credits)
### Installation
Add entrance to your dependencies in `mix.exs`.
```elixir
def deps do
[{:entrance, "~> 0.4.3"}]
end
```
Then add the configuration to *[your_app/config/config.exs](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/config/config.exs)*
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app/accounts/user.ex)*
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/router.ex)*
```elixir
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:
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/controllers/user_controller.ex)* |`$ mix entrance.gen.phx_user_controller`
```elixir
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`):
```elixir
Entrance.User.create(Customer, customer_params)
```
And get their `create_changesets` too...
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/controllers/session_controller.ex)* |`$ mix entrance.gen.phx_session_controller`
```elixir
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`:
```elixir
Entrance.auth_by([email: email, admin: true], password)
```
More fields matching the same value, `Entrance.auth_one`:
```elixir
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`:
```elixir
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](https://hexdocs.pm/entrance/Entrance.html#content).
#### 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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/plugs/require_login.ex)* |`$ mix entrance.gen.phx_require_login`
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/router.ex)*:
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/lib/your_app_web/controllers/session_controller.ex)* |`$ mix entrance.gen.phx_session_controller`
```elixir
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](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/test/your_app_web/controllers/page_controller_test.exs)*
```elixir
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](https://github.com/BlakeWilliams/doorman). Thanks to [Blake Williams](https://github.com/blakewilliams) & [Ashley Foster](https://github.com/AshleyFoster).
For the logo, thanks to [Melissa Moreira](https://github.com/melissamoreira).