Table of Contents
Introduction
Getting Started
Installing and Configuring Dependencies
Creating the User Model
Creating the User Migration
Creating the Tokens Migration
Running the Migrations
Setting Up the Auth Routes
Creating the User Controller
Creating the Auth View
Creating the Auth Controller
Test Drive
Accessing Protected Routes
Introduction
Gotta have users. Well, at least you’d probably like to have some users if you’re building a web or mobile app. And let’s get real; what the hell would you be doing on this page if you’re not currently building or looking to build a web or mobile app?
We all know authentication and authorization are kind of a turd to get moving. So let’s stir the pot and drop that plunger once and for all.
Getting Started
Alright, let’s get going. We’re going to be building a JSON API backend that will be able to serve whatever client you’d like, whether it’s a single-page web application or a native mobile app. We’re also going to be using MySQL for my development environment because I’ve lost enough days of my life trying to install PostgreSQL on my machine and I’ll spare you that crisis. Pop open a terminal and initialize your application:
$ mix pheonix.new MyPhoenixBackend --no-brunch --no-html --database mysql
Once that’s finished, configure your database credentials in config/dev.exs
then run the following in your terminal:
$ mix ecto.create
Installing and Configuring Dependencies
Overview
Comeonin
We’ll be using Comeonin to hash the raw passwords provided by users when they sign up.
Guardian
We’ll be using Guardian to check every request (after a successful login) and make sure the user is authenticated/authorized. It’s important to keep in mind that Guardian does not have anything to do with the initial challenge of validating a user’s email and password. That’s up to you and Üeberauth. Once your user’s credentials are validated, Guardian will help create the token and check it on every subsequent request.
GuardianDb
We’ll be using GuardianDb as an additional layer on top of Guardian to store tokens in the database so we can verify that they’re still valid. Standard/pure JWT authentication/authorization does not persist state in the database. This sounds like a great idea and one that minimizes database queries, but it has some very serious negative consequences. Don’t even get me started.
Installation
What we should get started on is adding all these dependencies. Open up your project’s mix.exs
file and add everything to your deps
and application
blocks. They should end up looking something like this:
# mix.exs ... def application do [mod: {MyPhoenixBackend, []}, applications: [:phoenix, :phoenix_pubsub, :cowboy, :logger, :gettext, :phoenix_ecto, :mariaex, :comeonin, :guardian, :guardian_db]] end ... defp deps do [ {:phoenix, "~> 1.2.1"}, {:phoenix_pubsub, "~> 1.0"}, {:phoenix_ecto, "~> 3.0"}, {:mariaex, "~> 0.8.1", override: true}, {:gettext, "~> 0.11"}, {:cowboy, "~> 1.0"}, {:comeonin, "~> 3.0"}, {:guardian, "~> 0.14"}, {:guaridan_db, "~> 0.7.0"} ] end ...
In your terminal, run the following to fetch all the dependencies and compile your application:
$ mix deps.get, compile
Configuration
Now it’s time to configure those new dependencies. Open up config/config.exs
and make sure it looks something like this:
# config/config.exs ... config :guardian, Guardian, hooks: GuardianDb, issuer: "MyPhoenixBackend", ttl: { 365, :days }, allowed_drift: 2000, secret_key: to_string(Mix.env) <> "SoMe LoNg HaShy StRinG tHaT yOu CaN mAkE uP 1234567890!@#$%^&*()", serializer: MyPhoenixBackend.GuardianSerializer config :guardian_db, GuardianDb, repo: MyPhoenixBackend.Repo ...
The Guardian Serializer
You’ll need to provide a custom serializer for Guardian to do it’s job. First, create a directory called “token” inside of the “web” directory. Then create a file inside that new directory called guardian_serializer.ex
. Open it up in your text editor and add the following code. Feel free to tweak it any way you see fit:
# web/token/guardian_serializer.ex defmodule MyPhoenixBackend.GuardianSerializer do @behaviour Guardian.Serializer alias MyPhoenixBackend.Repo alias MyPhoenixBackend.User def for_token(user = %User{}), do: { :ok, "User:#{user.id}" } def for_token(_), do: { :error, "Unknown resource type" } def from_token("User:" <> id), do: { :ok, Repo.get(User, id) } def from_token(_), do: { :error, "Unknown resource type" } end
Creating the User Model
Run the following in your terminal:
$ mix phoenix.gen.json User users first_name:string email:string hashed_password:string
Here we’ll set up our User model to do a number of things. Feel free to skip the explanation. I do the same thing (“Give me the damn code!”). But here’s the skinny:
- First we’ll add a virtual
:password
field to the User model. It’s virtual because it’s not a field in the database users table and we won’t be storing it. It’s sensitive, okay? - Then we’ll create a private helper function called
put_password_hash
that will hash the provided password before we store it in the database just in case Sneaky Pete comes poking around. -
In order for that cool
put_password_hash
function to work, we’ll need to lean on the Comeonin module we installed as a dependency. So we’ll alias that for extreme convenience. -
Finally, we’ll add a changeset that we’ll name
registration_changeset
. This changeset will be used when we’re creating new users. It’ll do a handful of things including requiring specific attributes, checking that thepassword
andpassword_confirmation
are equal, validating the format and uniqueness of the user’s email and length of their password, as well as applying theput_password_hash
function we’ll be adding. Now, when you think “signing up” for a site/app, think creating a new user. Don’t confuse this with “signing in” to a site/app. We’ll get to that later but it has no business here. Signing up for an app will not in itself create a new authentication token. It simply creates a new user with verifiable credentials (like email and password) that will be checked later when logging in. If you deem it an appropriate user experience you can of course log your user in immediately after creating the account. Totally fine. But the concerns should be separate. Often sites will require a user to verify their account via an email sent to the new registree. In this case you don’t want to log your user in directly following the initial sign up. We’re not going to get that gritty here but the point remains that signing up and signing in are two very different things.
All in all, here’s what this little doosie should look like:
# web/models/user.ex defmodule MyPhoenixBackend.User do use MyPhoenixBackend.Web, :model alias Comeonin.Bcrypt schema "users" do field :first_name, :string field :email, :string field :hashed_password, :string field :password, :string, virtual: true timestamps() end def registration_changeset(user, params \\ :empty) do user |> cast(params, [:first_name, :email, :password]) |> validate_required([:first_name, :email, :password]) |> validate_confirmation(:password) |> validate_length(:password, min: 6) |> validate_format(:email, ~r/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}$/) |> unique_constraint(:email) |> put_password_hash end defp put_password_hash(changeset) do case changeset do %Ecto.Changeset{ valid?: true, changes: %{ password: pass } } -> put_change(changeset, :hashed_password, Comeonin.Bcrypt.hashpwsalt(pass)) _ -> changeset end end end
Creating the User Migration
The User migration file will already have been generated when you ran the mix phoenix.gen.json
command earlier in the terminal. We just want to add a unique index on the email field and not-null requirements on all fields. Here’s what the migration should look like:
# priv/repo/migrations/*_create_user.exs defmodule MyPhoenixBackend.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :first_name, :string, null: false add :email, :string, null: false add :hashed_password, :string, null: false timestamps() end create unique_index(:users, [ :email ]) end end
Creating the Tokens Migration
As of early 2017, the documentation for Üeberauth’s GuardianDb requires the addition of a table name guardian_tokens
. You can configure GuardianDb to use a different table name if you’d like. This is necessary to keep track of valid tokens. (Really, don’t get me started on using pure JWT for sessions. Sure, it would be fantastic if we could have a truly stateless server that also didn’t need to make a database query to authorize. But what if an account is compromised? Or even more simply, what if a user decides to stop being a paying customer? What do you do with those tokens then? Just wait for them to expire while that user still has full access to restricted portions of the site/app because their tokens are still totally valid in the eyes of the token-signing algorithm? I don’t think so. Or on the flip side, have token expiration timeframes be so short that your users constantly have to keep signing in? I don’t think so. Well – you say – you could keep a blacklist of invalid tokens, which by its very nature destroys the idea of pure JWT auth. Sure you could do that. Or you could keep a whitelist. I’m glass half full on this one.)
At any rate, let’s get that migration going. In the terminal:
$ mix ecto.gen.migration create_guardian_tokens
and in that freshly-minted migration file:
# priv/repo/migrations/*_create_guardian_tokens.exs defmodule MyPhoenixBackend.Repo.Migrations.CreateGuardianTokens do use Ecto.Migration def change do create table(:guardian_tokens, primary_key: false) do add :jti, :string, primary_key: true add :aud, :string, primary_key: true add :typ, :string add :iss, :string add :sub, :string add :exp, :bigint add :jwt, :text add :claims, :map timestamps end end end
Running the Migrations
In your terminal, run:
$ mix ecto.migrate
Setting Up the Auth Routes
# web/router.ex defmodule MyPhoenixBackend.Router do use MyPhoenixBackend.Web, :router pipeline :api do plug :accepts, ["json"] end pipeline :bearer_auth do plug Guardian.Plug.VerifyHeader, realm: "Bearer" plug Guardian.Plug.LoadResource end pipeline :ensure_auth do plug Guardian.Plug.EnsureAuthenticated end scope "/auth", MyPhoenixBackend do pipe_through :api post "/sign-in", AuthController, :sign_in post "/sign-up", UserController, :create end scope "/api", MyPhoenixBackend do pipe_through [ :api, :bearer_auth, :ensure_auth ] resources "/lists", ListController, except: [ :new, :edit ] end end
Creating the User Controller
Alright, we’re finally at the fun part! We’re going to build out the User controller to allow users to sign up for our site/app. Signing up for an app equates to inserting a new record in the users
table, which is why we’ve routed /auth/sign-up
to the create
function of the User controller.
We already have the web/controllers/user_controller.ex
file from when we ran mix phoenix.gen.json User...
before. Open it up in your text editor and edit the create
function to look like the following:
# web/controllers/user_controller.ex ... def create(conn, %{"user" => user_params}) do changeset = User.registration_changeset(%User{}, user_params) case Repo.insert(changeset) do {:ok, user} -> conn |> put_status(:created) |> render("show.json", user: user) {:error, changeset} -> conn |> put_status(:unprocessable_entity) |> render(MyPhoenixBackend.ChangesetView, "error.json", changeset: changeset) end end ...
The real workhorse here is the registration_changeset
that performs all the validations and hashes the raw password. This leaves the controller quite simple and readable.
You can now officially sign users up for your site/app! If you’d like to see this in action, start the Phoenix server from your terminal:
$ mix phoenix.server
Now, send a POST request to http://localhost:4000/auth/sign-up. You can do this on the command-line with curl
or with the super awesome and free tool Postman. Make sure you set the “Content-Type” header to “application/json” and format the body of your request to look something like this:
{ "user": { "first_name": "Kyle", "email": "kyle@mail.com", "password": "123456", "password_confirmation": "123456" } }
If everything has been plumbed successfully, you should get a response that looks like the following:
{ "data": { "id": 42, "first_name": "Kyle", "email": "kyle@mail.com" } }
Creating the Auth View
Now that we’ve handled signing up, let’s tackle signing in. We’ll need to create two new files for this: web/controllers/auth_controller.ex
and web/views/auth_view.ex
. Let’s start with the view:
# web/views/auth_view.ex defmodule MyPhoenixBackend.AuthView do use MyPhoenixBackend.Web, :view def render("show.json", %{ token: token, user_id: user_id }) do %{ token: token, user_id: user_id } end def render("401.json", %{ message: message }) do %{ errors: [ %{ id: "UNAUTHORIZED", title: "401 Unauthorized", detail: message, status: 401 } ] } end def render("403.json", %{ message: message }) do %{ errors: [ %{ id: "FORBIDDEN", title: "403 Forbidden", detail: message, status: 403 } ] } end def render("delete.json", _) do %{ ok: true } end end
This view file is nothing more than a handful of functions your controller will invoke in order to send JSON back to the client. The main purpose here is simply formatting the response.
Creating the Auth Controller
Now open up the controller in your text editor and make it look like the following:
# web/controllers/auth_controller.ex defmodule MyPhoenixBackend.AuthController do use MyPhoenixBackend.Web, :controller import Comeonin.Bcrypt, only: [ checkpw: 2, dummy_checkpw: 0 ] alias MyPhoenixBackend.User def sign_in(conn, params = %{ "email" => _, "password" => _ }) do case check_email_and_password(params) do { :ok, user } -> case Guardian.encode_and_sign(user) do {:ok, jwt, _claims} -> conn |> put_status(:created) |> render("show.json", %{ token: jwt, user_id: user.id}) {:error, :token_storage_failure} -> handle_unauthenticated(conn, "There was an error creating the session (:token_storage_failure)") {:error, reason} -> handle_unauthenticated(conn, reason) end { :error, reason } -> handle_unauthenticated(conn, reason) end end defp handle_unauthenticated(conn, reason) do conn |> put_status(:unauthorized) |> render("401.json", message: reason) end defp check_email_and_password(%{ "email" => email, "password" => password }) do user = User |> Repo.get_by(email: email) cond do user && checkpw(password, user.hashed_password) -> { :ok, user } user -> { :error, "The provided password doesn't match the provided email (#{email})" } true -> dummy_checkpw() { :error, "User does not exist with email #{email}" } end end end
This is easily the most complicated piece of logic we’ve seen so far. But really it just looks like a lot. Let’s break it down:
The sign_in
function checks that the email and password provided by the user are valid (by invoking the check_email_and_password
function), and either proceeds with the signing in or responds to the client that the login info is invalid. If the email/password combo is valid, then it uses Guardian to sign the user in. Under the hood, when the Guardian.encode_and_sign
function is called, Guardian and GuardianDb create a JWT and insert a corresponding record in the guardian_tokens
table. Then it returns a tuple with the status of the operation: { :ok, user }
if everything went well and { :error, ... }
if there was a problem. Based on this tuple, we then respond to the client accordingly.
That’s the long and the short of the sign in process.
For a bit more detail, the check_email_and_password
function first queries the users
table to find the user with the provided email. Then it runs through a cond
statement, which will evaluate the first block that follows a truthy statement. The first statement (user && checkpw(password, user.hashed_password)
) will be truthy if there exists a user in the users
table with an email that matches the one provided by the client and if the password provided matches the stored hashed_password
. In order to make this comparison, we use the Comeonin.Bcrypt library and invoke checkpw
. This will hash the newly provided password and check it against the stored hashed_password
. The second statement (user
) will be truthy if there exists a user in the users
table with an email that matches the one provided by the client but the password does not match the one stored in hashed_password
. The final statement (true
) acts as a catch-all and will be called when neither the provided email nor password are valid.
The handle_unauthenticated
function simply orchestrates a response back to the client that the request is unauthorized when something goes wrong.
And that’s it!
Test Drive
We can now sign up and sign in. Let’s do just that. Above, we sent a POST request to the /auth/sign-up
endpoint to register a user. If you haven’t done so already, go ahead and register a user.
Now, send a POST request (remembering to set the “Content-Type” header to “application/json”) to /auth/sign-in
with a request body that looks like this:
{ "email": "kyle@mail.com", "password": "123456" }
If everything goes according to plan, you should receive a response similar to this:
{ "user_id": 42, "token": "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJVc2VyOjQxIiwiZXhwIjoxNTE3MDY3MDMwLCJpYXQiOjE0ODU1MzEwMzAsImlzcyI6Ik15UGhvZW5peEJhY2tlbmQiLCJqdGkiOiIzYWVlZWE2OC1jMTg1LTRkMTgtYjY4YS1mZmRmOWVlOWVjYTIiLCJwZW0iOnt9LCJzdWIiOiJVc2VyOjQxIiwidHlwIjoiYWNjZXNzIn0.s8vzqiKoEHAQw0_Fl7t4kuDSRbdgTAPiKt2B7zKcKntKieA3msyNlF1YKovO-WkD4KUFfOwui2E9nKHNV6lr0Q" }
It’s now up to the client to store that token. In a browser-based app you can save it in a cookie, session storage, local storage, or any other way you’d like. You can even just keep it in memory of you want your users logged out immediately upon leaving your site.
Personally, I’m an Ember guy, so I lean towards Ember Simple Auth to easily handle the client’s portion of auth responsibility in a web app.
On the mobile app side of things, I can only point you to a NativeScript solution since that’s how I build native mobile apps: Application Settings. Here, onec you receive the token from your Phoenix backend, you’d store it in the “application-setting”.
Regardless of how you store the token, we’ll need to set it in the “Authorization” header of all subsequent requests we send to our Phoenix API. Specifically, here’s what the header should look like: Bearer: eyJhbGciOiJIUz...
. Note that we need to concatenate “Bearer: ” to the beginning of the token string.
Accessing Protected Routes
Now that we have our sign-up and sign-in systems in place, let’s build a resource that’s only accessible to authenticated users. In your terminal, run the following:
$ mix phoenix.gen.json List lists title:string user_id:references:users
Leave a Reply