# SansPassword
A simple, passwordless authentication system based on [Guardian](https://github.com/ueberauth/guardian).
SansPassword supports two different authentication flows:
+ _Login_ - When a user enters their email address, if their account exists, they'll be sent an email containing a link to login.
+ _Register_ - When a user enters their email address, if their account does not exist, they'll be sent an email containing a link. When they click the link, an account will be created using the provided email address, and they'll be signed in.
See the source code for the demo app [here](https://github.com/promptworks/sans_password_demo).
## Installation
1. Add `sans_password` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:sans_password, "~> 0.1.0"}]
end
```
2. Ensure bamboo is started before your application:
```elixir
def application do
[applications: [:sans_password]]
end
```
## Usage
First, you'll need to configure `sans_password` and `guardian`. A minimal configuration looks like this:
```elixir
config :sans_password, SansPassword,
repo: MyApp.Repo,
schema: MyApp.User,
mailer: SansPassword.Adapters.Bamboo
config :sans_password, SansPassword.Adapters.Bamboo,
emails: MyApp.Emails,
mailer: MyApp.Mailer
config :guardian, Guardian,
issuer: "MyApp",
ttl: {30, :days},
secret_key: "super secret key!",
serializer: SansPassword.Serializer
```
You'll want to look at [Guardian's documentation](https://github.com/ueberauth/guardian) for all of it's configuration options.
The configuration above uses Bamboo for emails, but you could very easily implement your own adapter.
### Controllers/Views
SansPassword includes a macro for creating a controller. You'll need to tell it which view to use to render templates, as well as which module to use for hooks.
`SansPassword.Hooks` brings in a behaviour that will tell you which functions need to be implemented.
```elixir
# web/views/session_controller.ex
defmodule MyApp.SessionController do
use MyApp.Web, :controller
use SansPassword.Hooks
use Passwordles.Controller, view: MyApp.SessionView, hooks: __MODULE__
# SansPassword.Hooks requires that you implement the following functions:
def after_invite_path(conn, _params), do: session_path(conn, :new)
def after_invite_failed_path(conn, _params), do: session_path(conn, :new)
def after_login_path(conn, _params), do: page_path(conn, :index)
def after_login_failed_path(conn, _params), do: session_path(conn, :new)
def after_logout_path(conn, _parms), do: session_path(conn, :new)
end
```
You'll just need to create a view:
```elixir
# web/views/session_view.ex
defmodule MyApp.SessionView do
use MyApp.Web, :view
end
```
Then, create a template for the login form:
```eex
<!-- web/templates/new.html.eex -->
<%= form_for @conn, session_path(@conn, :create), [as: :session], fn f -> %>
<div class="form-group">
<label for="session[email]" class="control-label">Email</label>
<%= text_input f, :email, class: "form-control" %>
</div>
<%= submit "Submit", class: "btn btn-primary" %>
<% end %>
```
### Routing
We'll need to add Guardian's plugs to our routes, and declare routes for our new session controller.
```elixir
# web/router.ex
pipeline :browser do
# ...
end
pipeline :browser_session do
plug Guardian.Plug.VerifySession
plug Guardian.Plug.LoadResource
end
pipeline :require_auth do
plug Guardian.Plug.EnsureAuthenticated
end
# Unauthenticated routes go here
scope "/", MyApp do
pipe_through [:browser, :browser_session]
get "/login", SessionController, :new
post "/login", SessionController, :create
get "/login/callback", SessionController, :callback
end
# Authenticated routes go here
scope "/", MyApp do
pipe_through [:browser, :browser_session, :require_auth]
get "/logout", SessionController, :delete
end
```
### Mailers
Here's an example Emails module using Bamboo:
```elixir
# web/emails.ex
defmodule MyApp.Emails do
import Bamboo.Email
use Bamboo.Phoenix, view: MyApp.EmailView
@from "admin@myapp.com"
def login(user, params) do
new_email
|> from(@from)
|> to(user.email)
|> subject("Login to MyApp")
|> assign(:user, user)
|> assign(:params, params)
|> render(:login)
end
def register(email, params) do
new_email
|> from(@from)
|> to(email)
|> subject("Register with MyApp")
|> assign(:params, params)
|> render(:register)
end
end
```
When rendering the email template, all that matters is that you include the login link like so:
```eex
<%= link "Click here to login", to: session_url(MyApp.Endpoint, :callback, @params) %>
```
### Trackable (optional)
Add the required fields in a migration:
```elixir
alter table(:users) do
add :sign_in_count, :integer, default: 0
add :last_sign_in_ip, :string
add :last_sign_in_at, :datetime
add :current_sign_in_ip, :string
add :current_sign_in_at, :datetime
end
```
Make some minor tweaks to your model:
```elixir
defmodule MyApp.User do
use SansPassword.Schema
schema "users" do
# ...
trackable_fields()
end
end
```
Configure Guardian to use `SansPassword.Trackable` module:
```elixir
config :guardian, Guardian,
hooks: SansPassword.Trackable
```
### Accessing the current user
Under the hood, SansPassword just uses Guardian, so to get the current user, just say:
```elixir
Guardian.Plug.current_resource(conn)
```