Current section
Files
Jump to
Current section
Files
README.md
# EntranceModules and functions to make authentication with *Plug / Phoenix* and *Ecto* easy without tons of configuration or boxing users into rigid framework. 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.If you like simple and beautiful code, you'd like *Entrance*.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)- [Contribute](#contribute)- [Credits](#credits)### InstallationAdd entrance to your dependencies in `mix.exs`.```elixirdef deps do [{:entrance, "~> 0.4.0"}]end```Then add the configuration to *[your_app/config/config.exs](https://github.com/henriquefernandez/entrance/blob/master/examples/your_app/config/config.exs)*```elixirconfig :entrance, repo: YourApp.Repo, security_module: Entrance.Auth.Bcrypt, user_module: YourApp.Accounts.User, default_authenticable_field: :email```### PhoenixFirst, 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)* ```elixirdefmodule 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 endend```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 UsersTo 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:```elixirdefmodule 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````elixirdefmodule 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`):```elixirEntrance.User.create(Customer, customer_params)```And get their `create_changesets` too...```elixirEntrance.User.create_changeset(Customer)```#### Logging in usersTo 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````elixirdefmodule 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`:```elixirEntrance.auth_by([email: email, admin: true], password)```More fields matching the same value, `Entrance.auth_one`:```elixirEntrance.auth_one([:email, :nickname], my_nickname, password)```More fields matching the same value, and more fields matching the user schema `Entrance.auth_one_by`:```elixirEntrance.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 AuthenticationTo 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````elixirdefmodule 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 endend```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)*:```elixirpipeline :protected do plug YourAppWeb.Plugs.RequireLoginend # ...scope "/protected", YourAppWeb do pipe_through :browser pipe_through :protected get "/", PageController, :protectedend # ...```#### Logging out usersTo 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````elixirdefmodule 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: "/") endend ```#### TestingYou 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)*```elixirdefmodule 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! endend```## 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*## CreditsEntrance 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).