Guard this with your life... Or authenticating APIs with Guardian

Authentication in Phoenix

Conceptually, authentication isn’t hard. You collect a username and password, check it against your database and if it matches, WIN!

getinsertpic.com

… then you get into the business of persisting that information across requests, not to mention all of the security concerns of storing a password safely. In this post we’ll be covering how to authenticate users, provide them with a token, and use that token on subsequent requests to identify our user. There are a TON of moving parts, so strap in.

Overview

We’ll be covering:

The App

We’ll be expanding on our Article Tracker located here.

Accounts

Accounts will have an email and a password_digest. The digest will hold our hashed password… eventually.

Run mix phoenix.gen.model Account accounts email password_digest then mix ecto.migrate.

Open the new model, and update the schema to have a virtual attribute:

1
2
3
4
5
6
7
schema "accounts" do
  field :email, :string
  field :password_digest, :string
  field :password, :string, virtual: true # <- New line

  timestamps
end

Virtual attributes are a great way of taking a value from an interaction, but not persisting. In our case, we’ll be using it to hold the value a users gives us before we hash and persist it.

Changesets

Changesets are a way of describing the process a set of proposed changes go through in preparation to being persisted. You define your rules, then try to persist. If everything is good to go, say passing all of your validation rules, it can be inserted into your database.

Storing passwords in plain text is a bad, NAY TERRIBLE IDEA!! We want to allow a user to send us a password, that we’ll hash then store.

Creating a changeset is easy. In the model, we call cast and pass in the model, a map with proposed values, and the fields we require, and permit.

This is the built in one from our project. Be sure to change the password_digest in our @required_fields to password. The user will never pass us a password_digest

1
2
3
4
5
6
7
@required_fields ~w(email password) # <- Changed from password_digest to password
@optional_fields ~w()

def changeset(model, params \\ :empty) do
  model
  |> cast(params, @required_fields, @optional_fields)
end

This returns a changeset back. We can tag along another function, as long as it returns a changeset as well. Let’s do some work to obfuscate the user’s password.

1
2
3
4
5
6
7
8
9
10
def changeset(model, params \\ :empty) do
  model
  |> cast(params, @required_fields, @optional_fields)
  |> put_pass_hash
end

defp put_pass_hash(changeset) do
  password = changeset.changes.password
  put_change(changeset, :password_digest, String.reverse(password))
end

Open up iex -S mix and try this out:

1
2
3
iex> alias ArticleTrackerHd.Account
iex> changeset = Account.changeset(%Account{}, %{email: "steven@example.com", password: "beef101"})
iex> changeset.changes.password_digest # => "101feeb"

#security #hashtag. I think we need to be a bit more deliberate in our approach.

Come on in!

Maybe not what you want to hear in a security library, but, hey… We’ll be adding this library to handle our password hashing.

Checkout their github and add the latest version to your mix.exs file.

1
2
3
4
5
6
7
8
9
10
11
12
defp deps do
  [{:phoenix, "~> 1.1.4"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_ecto, "~> 2.0"},
   {:phoenix_html, "~> 2.4"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.9"},
   {:comeonin, "~> 2.4"}, # <- New line
   {:cowboy, "~> 1.0"}]
end


Then start the application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def application do
  [mod: {ArticleTrackerHd, []},
   applications: [
     :phoenix,
     :phoenix_html,
     :cowboy,
     :logger,
     :gettext,
     :phoenix_ecto,
     :postgrex,
     :comeonin # <- New Line
    ]
  ]
end

mix deps.get and we’re good to go.

Updating our changeset

Let’s update to our super secure hashing algorithm.

1
2
3
4
defp put_pass_hash(changeset) do
  password = changeset.changes.password
  put_change(changeset, :password_digest, Comeonin.Bcrypt.hashpwsalt(password))
end

Open up iex -S mix and try again:

1
2
3
4
iex> alias ArticleTrackerHd.{Account, Repo}
iex> changeset = Account.changeset(%Account{}, %{email: "steven@example.com", password: "beef101"})
iex> changeset.changes.password_digest # => "Something crazy"
iex> Repo.insert!(changeset)

WIN! We’re doing right by our users. Note that we saved user with an email of steven@example.com and a password of beef101.

Authenticating users

If you’re coming from rails, you might be tempted to slap on an authenticate function and have it find the account, check the password and all that jazz. Well HOLD YOUR HORSES! getinsertpic.com

In Phoenix, we create “pure” function in our models and views, and “impure” functions… elsewhere. What is a “pure” function? Think of a function that takes in 2 values

1
fn (a, b) -> a + b end

If you call this function with 1 and 2, the result will always be the same. Now imagine calling this code:

1
Repo.all(Account)

This code might return 1 User, or 300. We’re touching the outside world. Fancy terms for simple things.

Back to authentication. We need a place to handle our impure operations. I’m coining a new term, We need a Service Module

getinsertpic.com

This module will hold our impure auth related stuff. Make a new file in web/services called auth.ex.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
defmodule ArticleTrackerHd.Auth do
  alias ArticleTrackerHd.{Repo, Account}
  import Comeonin.Bcrypt, only: [checkpw: 2, dummy_checkpw: 0]

  def verify(email, password) do
    account =  Repo.one(Account.find_by_email(email)) # <- Need to define find_by_email
    cond do
      account && checkpw(password, account.password_digest) -> # <- Valid account and password!
        {:ok, account}
      account -> # <- Found account but password didn't match
        {:error, :bad_password}
      true ->
        dummy_checkpw # <- Fool hackers
        {:error, :not_found}
    end
  end
end

So IMPURE! We have one nugget of purity and that’s in the find_by_email function. That will return the query we’ll pass to Repo.one. Here it is:

1
2
3
4
def find_by_email(email) do
  from a in __MODULE__,
  where: a.email == ^email
end

Try it out. iex -S mix.

1
2
3
4
5
6
7
8
9
10
11
12
iex> alias ArticleTrackerHd.{Account, Auth}
iex> Auth.verify("steven@example.com", "beef101")
{:ok,
 %ArticleTrackerHd.Account{__meta__: #Ecto.Schema.Metadata<:loaded>,
  email: "steven@example.com", id: 1,
  inserted_at: #Ecto.DateTime<2016-05-24T02:29:27Z>, password: nil,
  password_digest: "$2b$12$vEJ80G.uEpJapbXgsGHBN.aZSgDD3yyWiQm.gimOSZimApDL.pTOm",
  updated_at: #Ecto.DateTime<2016-05-24T02:29:27Z>}}
iex> Auth.verify("steven@example.com", "wrongpw")
{:error, :bad_password}
iex> Auth.verify("nope@example.com", "wrongpw")  
{:error, :not_found}

GLORIOUS!!

Guardian

Guardian is a way of generating JWT tokens. Tokens can be read from the session, or from the headers. We’re designing this to work with an API, so we’ll check the headers.

Guardian doesn’t manage finding accounts, that’s your job, hence all the blah blah from before. Once that’s all set up, we’ can leverage Guardian to manage keeping our user requests and our app in sync.

Let’s set it up. You’ll need a key for the next part. Run mix phoenix.gen.secret and hold on to the value.

Open up mix.exs:

1
2
3
4
5
6
7
8
9
10
11
defp deps do
  [{:phoenix, "~> 1.1.4"},
   {:postgrex, ">= 0.0.0"},
   {:phoenix_ecto, "~> 2.0"},
   {:phoenix_html, "~> 2.4"},
   {:phoenix_live_reload, "~> 1.0", only: :dev},
   {:gettext, "~> 0.9"},
   {:comeonin, "~> 2.4"},
   {:guardian, "~> 0.10.0"}, # <- New line
   {:cowboy, "~> 1.0"}]
end

Run mix deps.get to get setup.

We’ll need to create a configuration for Guardian in our config.exs.

1
2
3
4
5
config :guardian, Guardian,
  issuer: "ArticleTrackerHd",
  ttl: { 30, :days },
  secret_key: "46l5yfRFGorxAArf64nGzHlfvSDAOUEV7m6c3/lf4LzZcBfUClDsSfNETrosmsdO", # <- Your Key
  serializer: ArticleTrackerHd.GuardianSerializer # <- We'll create this next

OK, so a bunch of stuff is about to happen… Brace yourself.

getinsertpic.com

We’re going to add 2 plugs. One to teach Guardian how to find our token, and another that lets us convert the token to an account.

Open up your router and add:

1
2
3
4
5
pipeline :api do
  plug :accepts, ["json"]
  plug Guardian.Plug.VerifyHeader, realm: "Bearer" # <- New line (1)
  plug Guardian.Plug.LoadResource                  # <- New line (2)
end

The first line tells Guardian to look in the header for Authorization: Bearer some_crazy_token. If you didn’t provide a realm, it would expect Authorization: some_crazy_token.

The second line works in tandem with the not yet written ArticleTrackerHd.GuardianSerializer to convert a token value into a record.

Let’s go write that serializer. Create a file in web/serializers named guardian_serializer.ex

1
2
3
4
5
6
7
8
9
10
11
12
defmodule ArticleTrackerHd.GuardianSerializer do
  @behaviour Guardian.Serializer

  alias ArticleTrackerHd.Repo
  alias ArticleTrackerHd.Account

  def for_token(account = %Account{}), do: { :ok, "Account:#{account.id}" }
  def for_token(_), do: { :error, "Unknown resource type" }

  def from_token("Account:" <> id), do: { :ok, Repo.get(Account, id) }
  def from_token(_), do: { :error, "Unknown resource type" }
end

Nothing too crazy here. If you find a claim in the token like Account:1, find account with an id of 1.

That’s it for Guardian Setup!

Logging in

getinsertpic.com

Let’s tie this all together. We’ll create a SessionController with a create action. It will check if the username and password are valid and if it is, we’ll sign them in.

Modify your router:

1
2
3
4
5
scope "/api", ArticleTrackerHd do
  pipe_through :api
  resources "/articles", ArticleController, except: [:new, :edit]
  post "/log-in", SessionController, :create # <- New line
end

Then make a file in web/controllers/ named session_controller.ex.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
defmodule ArticleTrackerHd.SessionController do
  use ArticleTrackerHd.Web, :controller
  alias ArticleTrackerHd.Auth

  def create(conn, %{"account" => %{"email" => email, "password" => password}}) do
    case Auth.verify(email, password) do # <- 1
      {:ok, account} ->
         conn
         |> sign_in(account)
         |> render("login.json")
      {:error, _} ->
        conn
        |> put_status(:unprocessable_entity)
    end
  end

  defp sign_in(conn, account) do # <- 2
     conn
     |> Guardian.Plug.api_sign_in(account)
     |> add_jwt
  end

  defp add_jwt(conn) do # <- 3
    jwt = Guardian.Plug.current_token(conn)
    assign(conn, :jwt, jwt)
  end
end

Create a file named session_view.ex in web/views/:

1
2
3
4
5
6
7
defmodule ArticleTrackerHd.SessionView do
  use ArticleTrackerHd.Web, :view

  def render("login.json", %{jwt: jwt}) do
    %{ jwt: jwt }
  end
end

The important parts are are numbered.

  1. We’re using our Auth Module!
  2. We’re using the Guardian.Plug.api_sign_in function. This will call our GuardianSerializer and add the account to the token.
  3. This adds the generated jwt to the conn so we can use it in our login.json.

Let’s test it out!

1
2
curl --data "account[email]=steven@example.com&account[password]=beef101" http://localhost:4000/api/log-in
# {"jwt":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBY2NvdW50OjEiLCJleHAiOjE0NjY2NTQxMzUsImlhdCI6MTQ2NDA2MjEzNSwiaXNzIjoiQXJ0aWNsZVRyYWNrZXJIZCIsImp0aSI6IjhlOTE2ZjQ2LWM5MmEtNDBhMi1iYzE0LTY3Y2VjMmEyYzI1OSIsInBlbSI6e30sInN1YiI6IkFjY291bnQ6MSIsInR5cCI6InRva2VuIn0.OXK_Hu6uJRo3QClBdY05_xFuUdXK2NS3oTCRNZFd6yhyyl-ub1bjBZQV-flM3J6i7WIh1QUvTV4L7I6x72VADA"}

WOOOOOOOOT! Now we just need to make a request with that token, and we should be good to go.

User Autorization

So we’re logged in. Now we want to restrict creating new articles to logged in users, but we want all users to be able to see them.

Open up the ArticleTrackerHd.ArticleController.

1
2
3
4
5
6
7
8
9
defmodule ArticleTrackerHd.ArticleController do
  use ArticleTrackerHd.Web, :controller

  alias ArticleTrackerHd.Article

  plug Guardian.Plug.EnsureAuthenticated, [handler: ArticleTrackerHd.GuardianErrorHandler]  when action in [:create, :update, :delete] # <- New line
  plug :scrub_params, "article" when action in [:create, :update]
#...
end

This line makes it so only authenticated users can create articles. We’ll have to create a new controller for handling guardian errors. If it fails authentication, it will call the unauthenticated function in the module you give it.

Put this in web/controllers/guardian_error_handler:

1
2
3
4
5
6
7
8
9
10
defmodule ArticleTrackerHd.GuardianErrorHandler do
  use ArticleTrackerHd.Web, :controller
  alias ArticleTrackerHd.GuardianErrorView
  def unauthenticated(conn, _params) do
      conn
      |> put_status(:forbidden)
      |> render(GuardianErrorView, :forbidden)
    end
end

If it fails we want to render a view that returns a JSON message of forbidden. Make a new file in web/views named guardian_error_view.ex:

1
2
3
4
5
6
defmodule ArticleTrackerHd.GuardianErrorView do
  use ArticleTrackerHd.Web, :view
  def render("forbidden.json", _assigns) do
    %{message: "Forbidden"}
  end
end

Let’s try this out, then get a beer.

1
2
3
4
5
6
7
8
curl --data "account[email]=steven@example.com&account[password]=beef101" http://localhost:4000/api/log-in
# {"jwt":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBY2NvdW50OjEiLCJleHAiOjE0NjY2NTQ2NzUsImlhdCI6MTQ2NDA2MjY3NSwiaXNzIjoiQXJ0aWNsZVRyYWNrZXJIZCIsImp0aSI6IjBjZDNiYWZhLTY4MjYtNGJlNi1hYWM3LTMzZWJkNTAwMzM5NyIsInBlbSI6e30sInN1YiI6IkFjY291bnQ6MSIsInR5cCI6InRva2VuIn0.S3jQkmtia-mlAv0O0u1QZxWr3MFH5IBnG1C_u0Uyjq6-DOA5is3l8tKiI0M83Vfw5ADUi55uXfoRqYRY7EQ-DQ"}
curl http://localhost:4000/api/articles
# all the articles
curl --data "article[url]=http://example.com&article[title]=A+Great+one&article[categories]=foo.bar.baz" http://localhost:4000/api/articles
# Forbidden
curl --data "article[url]=http://example.com&article[title]=A+Great+one&article[categories]=foo.bar.baz" http://localhost:4000/api/articles --header "Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBY2NvdW50OjEiLCJleHAiOjE0NjY2NTQ2NzUsImlhdCI6MTQ2NDA2MjY3NSwiaXNzIjoiQXJ0aWNsZVRyYWNrZXJIZCIsImp0aSI6IjBjZDNiYWZhLTY4MjYtNGJlNi1hYWM3LTMzZWJkNTAwMzM5NyIsInBlbSI6e30sInN1YiI6IkFjY291bnQ6MSIsInR5cCI6InRva2VuIn0.S3jQkmtia-mlAv0O0u1QZxWr3MFH5IBnG1C_u0Uyjq6-DOA5is3l8tKiI0M83Vfw5ADUi55uXfoRqYRY7EQ-DQ"
curl http://localhost:4000/api/articles

VICTORY!

Quick associations

Let’s add a reference to users on articles. Then we’ll create them and attribute them.

Run mix ecto.gen.migration add_account_reference_to_article

Then modify it to say:

1
2
3
4
5
6
7
8
9
defmodule ArticleTrackerHd.Repo.Migrations.AddAccountReferenceToArticle do
  use Ecto.Migration

  def change do
    alter table(:articles) do
      add :account_id, references(:accounts)
    end
  end
end

Wire up the models: In article.ex:

1
2
3
4
5
6
7
8
9
10
  schema "articles" do
    field :title, :string
    field :url, :string
    field :categories, :string
    belongs_to :account, ArticleTrackerHd.Account # <- New line

    timestamps
  end

And in account.ex:

1
2
3
4
5
6
7
8
schema "accounts" do
  field :email, :string
  field :password_digest, :string
  field :password, :string, virtual: true
  has_many :articles, ArticleTrackerHd.Article # <- New Line

  timestamps
end

Then in our ArticleController, we need to find the user and create an associated:

1
2
3
4
5
6
7
def create(conn, %{"article" => article_params}) do
  account = Guardian.Plug.current_resource(conn) # <- New line
  article = Ecto.build_assoc(account, :articles) # <- New line
  changeset = Article.changeset(article, article_params) # <- New line
  # ...

end

Try it one more time

1
curl --data "article[url]=http://example.com&article[title]=A+Great+one&article[categories]=foo.bar.baz" http://localhost:4000/api/articles --header "Authorization: Bearer eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJBY2NvdW50OjEiLCJleHAiOjE0NjY2NTQ2NzUsImlhdCI6MTQ2NDA2MjY3NSwiaXNzIjoiQXJ0aWNsZVRyYWNrZXJIZCIsImp0aSI6IjBjZDNiYWZhLTY4MjYtNGJlNi1hYWM3LTMzZWJkNTAwMzM5NyIsInBlbSI6e30sInN1YiI6IkFjY291bnQ6MSIsInR5cCI6InRva2VuIn0.S3jQkmtia-mlAv0O0u1QZxWr3MFH5IBnG1C_u0Uyjq6-DOA5is3l8tKiI0M83Vfw5ADUi55uXfoRqYRY7EQ-DQ"

And now it’s all associated.

getinsertpic.com

Conclusion

And that’s it! If you’re thinking, “MAN THAT WAS NUTS!”, I agree. A lot of the documentation is hard to find. I’m hoping to see more posts from YOU dear reader. Together we can make working with Phoenix and Elixir awesome.

I hope you find this useful when building Phoenix APIs! If you want to checkout the finished code, checkout the repo.

Found this interesting? Be sure to share it!

comments powered by Disqus