inaka

Latest blog entries

/
The Art of Writing a Blogpost

The Art of Writing a Blogpost

Mar 09 2017 : Matias Vera

/
SpellingCI: No more spelling mistakes in your markdown flies!

Feb 14 2017 : Felipe Ripoll

/
Fast reverse geocoding with offline-geocoder

Do you need a blazing fast reverse geocoder? Enter offline-geocoder!

Jan 18 2017 : Roberto Romero

/
Using Jayme to connect to the new MongooseIM REST services

MongooseIM has RESTful services!! Here I show how you can use them in an iOS application.

Dec 13 2016 : Sergio Abraham

/
20 Questions, or Maybe a Few More

20 Questions, or Maybe a Few More

Nov 16 2016 : Stephanie Goldner

/
The Power of Meeting People

Because conferences and meetups are not just about the technical stuff.

Nov 01 2016 : Pablo Villar

/
Finding the right partner for your app build

Sharing some light on how it is to partner with us.

Oct 27 2016 : Inaka

/
Just Play my Sound

How to easily play a sound in Android

Oct 25 2016 : Giaquinta Emiliano

/
Opening our Guidelines to the World

We're publishing our work guidelines for the world to see.

Oct 13 2016 : Brujo Benavides

/
Using NIFs: the easy way

Using niffy to simplify working with NIFs on Erlang

Oct 05 2016 : Hernan Rivas Acosta

/
Function Naming In Swift 3

How to write clear function signatures, yet expressive, while following Swift 3 API design guidelines.

Sep 16 2016 : Pablo Villar

/
Jenkins automated tests for Rails

How to automatically trigger rails tests with a Jenkins job

Sep 14 2016 : Demian Sciessere

/
Erlang REST Server Stack

A description of our usual stack for building REST servers in Erlang

Sep 06 2016 : Brujo Benavides

/
Replacing JSON when talking to Erlang

Using Erlang's External Term Format

Aug 17 2016 : Hernan Rivas Acosta

/
Gadget + Lewis = Android Lint CI

Integrating our Android linter with Github's pull requests

Aug 04 2016 : Fernando Ramirez and Euen Lopez

/
Passwordless login with phoenix

Introducing how to implement passwordless login with phoenix framework

Jul 27 2016 : Thiago Borges

/
Beam Olympics

Our newest game to test your Beam Skills

Jul 14 2016 : Brujo Benavides

/
Otec

Three Open Source Projects, one App

Jun 28 2016 : Andrés Gerace

/
CredoCI

Running credo checks for elixir code on your github pull requests

Jun 16 2016 : Alejandro Mataloni

/
Thoughts on rebar3

Thoughts on rebar3

Jun 08 2016 : Hernán Rivas Acosta

/
See all Inaka's blog posts >>

/
Passwordless login with phoenix

A photo of Thiago Borges wrote this on July 27, 2016 under authentication, elixir, phoenix .

Introduction

Passwordless login is, as you might have guessed, a login without a password. The flow is similar to "Recover password": you submit an email address and you receive an URL to access the system. As simple as that.

This process became more popular when medium started offering this login strategy instead of using a password, which has some security complexities, or login with Twitter or Facebook, which is not that simple and not everyone uses or trusts this way of login on any system.

You can get more posts explaining why passwords are not always a good option on Signing in to Medium by email and Why passwords suck.

It is also very hard for the user to manage many emails and passwords.

On this example, I used:

  • Elixir 1.3
  • Phoenix 1.2
  • Ecto 2.0

First step

To begin with, you need to start a new phoenix project mix phoenix.new passwordless_login_app, where passwordless_login_app is the application's name.

Now that it is up and running, you need to create a user model with an email and an access_token.

mix phoenix.gen.model User users email:string access_token:string

It creates the model, migration and test files. Open the migration one and include unique indexes for access_token and email.

defmodule PasswordlessLoginApp.Repo.Migrations.CreateUser do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string
      add :access_token, :string

      timestamps()
    end

    create unique_index(:users, [:access_token])
    create unique_index(:users, [:email])
  end
end

The indexes are important because we will search for both fields, and they are unique to prevent duplication on database level. Application uniqueness constraint is not enough.

Now open the user model on web/models/user.ex and change the changeset function.

def changeset(struct, params \\ %{}) do
  struct
  |> cast(params, [:email, :access_token])
  |> update_change(:email, &String.downcase/1)
  |> validate_required([:email])
  |> unique_constraint(:email)
  |> unique_constraint(:access_token)
end

update_change/3 normalizes all email inputs to make them downcase and avoid having Person@example.com and person@example.com as 2 different users.

unique_constraint/3 relies on database to check if the constraint has been violated. If so, Ecto converts it into a changeset error.

Create the sign in page

Now it is time to create some HTML, starting with the route. Open web/router.ex and add the resources /session for new and create actions.

scope "/", PasswordlessLoginApp do
  pipe_through :browser

  get "/", PageController, :index
  resources "/session", SessionController, only: [:new, :create]
end

Note that the create action is not implemented on this step, but we reference to it on the new.html form below.

Now create web/controllers/session_controller.ex exposing the user changeset to new.html template in order to be used on the form.

defmodule PasswordlessLoginApp.SessionController do
  use PasswordlessLoginApp.Web, :controller
  alias PasswordlessLoginApp.User

  def new(conn, _params) do
    changeset = User.changeset(%User{})
    render conn, "new.html", changeset: changeset
  end
end

You also need to create the SessionView and session/new.html.

At this point, the login/sign up page looks like this:

Create user and generate access token

Now that we already have the form with the email, it is time to work on create action, that is responsible for create user |> generate token |> send email or find user |> generate token |> send email.

The first place we have to change is the user model web/models/user.ex.

def registration_changeset(struct, params \\ %{}) do
  struct
  |> changeset(params)
  |> generate_access_token
end

defp generate_access_token(struct) do
  token = SecureRandom.hex(30)

  case Repo.get_by(__MODULE__, access_token: token) do
    nil ->
      put_change(struct, :access_token, token)
    _ ->
      generate_access_token(struct)
  end
end

Here we create a specific changeset for user registration. It reuses the default changeset and includes the access token generation.

generate_access_token/1 generates a 30 characters' token using secure_random and checks if this token is already associated with a user (very unlikely to happen). After case Repo.get_by(__MODULE__, access_token: token), if the access token is not used for any user, put_change/3 assigns the access_token to the current user. If a user is found using the generated token, we call generate_access_token/1 recursively until there is no user with the token.

Now that the access token has been generated, it is time to work on create action.

def create(conn, %{"user" => user_params}) do
  user_email = String.downcase(user_params["email"])
  user_struct =
    case Repo.get_by(User, email: user_email) do
      nil -> %User{email: user_email}
      user -> user
    end
    |> User.registration_changeset(user_params)

  case Repo.insert_or_update(user_struct) do
    {:ok, _} ->
      conn
      |> put_flash(:info, "We sent you a link to create an account. Please check your inbox.")
      |> redirect(to: page_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

We search for a user using the downcase submitted email. When there is no user, we initialize a struct with this email, and if the user is found, we simply return it. The next step is passing this struct to the User.registration_changeset/2 that we implemented in the previous step. After assigning it to user_struct, we try to update or create the record, depending if it already exists or if it is a new record.

Login

The next step is to create an action to make the user login. The URL for it will be something like http://example.com/session/438c6e01be. As soon as the user access the page, it will find the user by the access token 438c6e01be and assign the user id to the browser session.

First, we include the show action on the router file web/router.ex.

scope "/", PasswordlessLoginApp do
  ...
  resources "/session", SessionController, only: [:new, :create, :show]
end

The show action for the session controller is the following:

def show(conn, %{"id" => access_token}) do
  case Repo.get_by(User, access_token: access_token) do
    nil ->
      conn
      |> put_flash(:error, "Access token not found or expired.")
      |> redirect(to: page_path(conn, :index))
    user ->
      conn
      |> put_session(:user_id, user.id)
      |> put_flash(:info, "Welcome #{user.email}")
      |> redirect(to: page_path(conn, :index))
  end
end

It searches for a user with access token. When the user is not found, it simply adds an error flash message and redirects it to the first page. When the user is found, it sets the user id on session, add a success message on a flash message and redirect it to the first page.

Assign current_user

We can check if the user_id is present in the session and assign a current_user to the conn struct on every request using a plug.

You can create the file web/controllers/simple_auth.ex like this:

defmodule PasswordlessLoginApp.SimpleAuth do
  import Plug.Conn
  alias PasswordlessLoginApp.{Repo, User}

  def init(opts), do: opts

  def call(conn, _opts) do
    user_id = get_session(conn, :user_id)
    assign_current_user(conn, user_id)
  end

  defp assign_current_user(conn, nil) do
    assign(conn, :current_user, nil)
  end
  defp assign_current_user(conn, user_id) do
    user = Repo.get(User, user_id)
    assign(conn, :current_user, user)
  end
end

assign_current_user/2 assigns the current_user value as nil when there is no user_id assigned. This is necessary when we have to check for current_user on layout for example. The second case fetches the user from the database and assigns it into conn.

After the plug is created, we can add it into our scope "/" pipeline on router.ex.

scope "/", PasswordlessLoginApp do
  pipe_through [:browser, PasswordlessLoginApp.SimpleAuth]
  ...
end

Logout

Like every good system, we also have a logout function. Fortunately, the process for this is also quite simple.

First, we have to add a new statement for session resources on router.

scope "/", PasswordlessLoginApp do
  ...
  resources "/session", SessionController, only: [:new, :create, :show]
  resources "/session", SessionController, only: [:delete], singleton: true
end

We can't add a delete action in the first resources because it will create a route for DELETE /session/:id, and we only need to delete a single session DELETE /session; so, we included the singleton option.

On web/controllers/simple_auth.ex, we create a function to clean the session.

def logout(conn) do
  conn |> configure_session(drop: true)
end

And we use the SimpleAuth.logout/1 function on delete action.

def delete(conn, _params) do
  conn
  |> PasswordlessLoginApp.SimpleAuth.logout()
  |> put_flash(:info, "User logged out.")
  |> redirect(to: page_path(conn, :index))
end

We have to reference the delete action as <%= link "Logout", to: session_path(@conn, :delete), method: "delete" %> on template, and phoenix creates a form to be submitted as DELETE.

Send email with token

Now that the flow is working properly, it is time to send the email with the user token. I will show the steps to use mailgun, but other email services will have similar steps.

The first thing is to create the account on mailgun. After this, add mailgun on mix.exs and install using mix deps.get. If you get any error, include {:poison, "~> 2.1", override: true}. Your deps function on mix.exs will look like this:

defp deps do
  [{:phoenix, "~> 1.2.0"},
   {:phoenix_pubsub, "~> 1.0"},
   {:phoenix_ecto, "~> 3.0"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_html, "~> 2.6"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.11"},
   {:secure_random, "~> 0.5"},
   {:mailgun, "~> 0.1.2"},
   {:poison, "~> 2.1", override: true},
   {:cowboy, "~> 1.0"}]
end

After that, we have to set the credentials in a configuration file. Create config/dev.secret.exs, add it to .gitignore, because credentials should never be included in the repository, and add the following line at the end of config/dev.exs:

import_config "dev.secret.exs"

The file dev.secret.exs will contain something like this:

use Mix.Config

config :passwordless_login_app,
       mailgun_domain: "https://api.mailgun.net/v3/sandboxe7c419b.mailgun.org",
       mailgun_key: "key-7c419b7c457c419e1981"

We will also add a configuration for the production environment, but it depends on how you deploy your application. I load the credentials from environment variables, so I include the following configuration above import_config "prod.secret.exs" on config/prod.exs.

config :passwordless_login_app,
       mailgun_domain: System.get_env("MAILGUN_DOMAIN"),
       mailgun_key: System.get_env("MAILGUN_API_KEY")

The next step is to create the email client module. Create lib/passwordless_login_app/mailer.ex with the following content:

defmodule PasswordlessLoginApp.Mailer do
  alias PasswordlessLoginApp.{Endpoint, Router, User}
  use Mailgun.Client,
      domain: Application.get_env(:passwordless_login_app, :mailgun_domain),
      key: Application.get_env(:passwordless_login_app, :mailgun_key)

  def send_login_token(%User{email: email, access_token: token}) do
    send_email to: email,
    from: "noreplay@example.com",
    subject: "Your token",
    text: "Access your account #{token_url(token)}"
  end

  defp token_url(token) do
    Router.Helpers.session_url(Endpoint, :show, token)
  end
end

This file sets the mailgun configuration using the credentials we set on dev.secret.exs and config/prod.exs.

Now that everything is configured and we have the mailer ready, we only have to send the email when the user requests it. On SessionController#create we include Task.async(fn -> Mailer.send_login_token(user) end) after a successful result for Repo.insert_or_update(user_struct) to not stop the response while sending the email. Remember to replace {:ok, _} -> with {:ok, user} -> because now we are using the result of insert_or_update operation. The final SessionController#create will look like this:

def create(conn, %{"user" => user_params}) do
  user_struct =
    case Repo.get_by(User, email: user_params["email"]) do
      nil -> %User{email: user_params["email"]}
      user -> user
    end
    |> User.registration_changeset(user_params)

  case Repo.insert_or_update(user_struct) do
    {:ok, user} ->
      Task.async(fn -> Mailer.send_login_token(user) end)
      conn
      |> put_flash(:info, "We sent you a link to create an account. Please check your inbox.")
      |> redirect(to: page_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

Final touch

For security reasons, it is important to renew the session id for the cookie when setting the user id on SessionController#show. We will also move the piece of code responsible for it to SimpleAuth.login/2.

Create the function login on web/controllers/simple_auth.ex.

def login(conn, user) do
  conn |> put_session(:user_id, user.id) |> configure_session(renew: true)
end

Now replace |> put_session(:user_id, user.id) from SessionController#show with |> SimpleAuth.login(user). The final show will look like this:

def show(conn, %{"id" => access_token}) do
  case Repo.get_by(User, access_token: access_token) do
    nil ->
      conn
      |> put_flash(:error, "Access token not found or expired.")
      |> redirect(to: page_path(conn, :index))
    user ->
      conn
      |> SimpleAuth.login(user)
      |> put_flash(:info, "Welcome #{user.email}")
      |> redirect(to: page_path(conn, :index))
  end
end

Conclusion

The user creation and authentication flow are finished. There are still some cases that we have to handle like token expiration by time and by access, disable button on submit and so on.

The application code is available on github.com/inaka/phoenix_passwordless_login.

What do you feel about this process? Would it fit to any application you've worked? Feel free to tell us what you think on the comments.

A photo of

Thiago Borges

Full Stack Developer