SansPassword
A simple, passwordless authentication system based on 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.
Installation
- Add
sans_passwordto your list of dependencies inmix.exs:
def deps do
[{:sans_password, "~> 0.1.0"}]
end
- Ensure bamboo is started before your application:
def application do
[applications: [:sans_password]]
end
Usage
First, you'll need to configure sans_password and guardian. A minimal configuration looks like this:
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 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.
# 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:
# web/views/session_view.ex
defmodule MyApp.SessionView do
use MyApp.Web, :view
end
Then, create a template for the login form:
<!-- 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.
# 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:
# 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:
<%= link "Click here to login", to: session_url(MyApp.Endpoint, :callback, @params) %>
Trackable (optional)
Add the required fields in a migration:
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:
defmodule MyApp.User do
use SansPassword.Schema
schema "users" do
# ...
trackable_fields()
end
end
Configure Guardian to use SansPassword.Trackable module:
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:
Guardian.Plug.current_resource(conn)