Andrew Fontaine

Mostly code

Validation of API Calls With Ecto

31 Dec 2017

After recently using elixir and phoenix to recreate the API for an old side project, I wanted to use Ecto’s embedded schemas, a schema that is not persisted to an underlying table, to validate the JSON coming into an API endpoint and get a workable domain object for the call before transforming it into the model persisted for the DB.

We’ll look at how I set up the sign-up and sign-in endpoints using the embeded schemas to validate the incoming JSON.

Sign-up Controller and Schema

First, I created a new file to hold the sign-up schema.

defmodule Knocker.Accounts.SignUp do
  use Ecto.Schema

  embedded_schema do
    field(:email, :string)
    field(:name, :string)
    field(:phone_number, :string)
    field(:password, :string, virtual: true)
    field(:password_confirmation, :string, virtual: true)
    field(:password_hash)
  end

  def changeset(%SignUp{} = sign_up, attrs) do
    sign_up
    |> cast(attrs, [:email, :name, :password, :password_confirmation, :phone_number])
    |> validate_required([:email, :name, :password, :password_confirmation, :phone_number])
    |> validate_confirmation(:password)
    # I'll touch on this later
    # |> put_pass_hash
  end
end

The embedded schema holds all the form fields of a sign up form, including the name, email, phone number, password and password confirmation fields. When Knocker.Accounts.SignUp.changeset/2 is called, the changes provided by attrs are cast to the SignUp struct, same as if I was working with a regular schema. This returns an Ecto.Changeset.t object:

iex(1)> alias Knocker.Accounts.SignUp
Knocker.Accounts.SignUp
iex(2)> sign_up = %{email: "foo@bar.com", name: "Foo Bar", phone_number: "15555551234", password: "test", password_confirmation: "test"}
%{
  email: "foo@bar.com",
  name: "Foo Bar",
  password: "test",
  password_confirmation: "test",
  phone_number: "15555551234"
}
iex(3)> SignUp.changeset(%SignUp{}, sign_up)
#Ecto.Changeset<
  action: nil,
  changes: %{
    email: "foo@bar.com",
    name: "Foo Bar",
    password: "test",
    password_confirmation: "test",
    phone_number: "15555551234"
  },
  errors: [],
  data: #Knocker.Accounts.SignUp<>,
  valid?: true
>

Now, the changeset struct will tell me if the entered changes are valid, and what the errors are if any. For instance, if I change the sign_up map:

iex(4)> sign_up = %{sign_up | password_confirmation: "wrong_password"}
%{
  email: "foo@bar.com",
  name: "Foo Bar",
  password: "test",
  password_confirmation: "wrong_password",
  phone_number: "15555551234"
}
iex(5)> SignUp.changeset(%SignUp{}, sign_up)
#Ecto.Changeset<
  action: nil,
  changes: %{
    email: "foo@bar.com",
    name: "Foo Bar",
    password: "test",
    password_confirmation: "wrong_password",
    phone_number: "15555551234"
  },
  errors: [
    password_confirmation: {"does not match confirmation",
     [validation: :confirmation]}
  ],
  data: #Knocker.Accounts.SignUp<>,
  valid?: false
>

Now that the password confirmation field is wrong, the Ecto changeset tells me as such.

Once I have the changeset, I must now find out how to get a struct out of the changeset without hitting the database, as this not a persisted object. Fortunately, there is Ecto.Changeset.apply_action/2, which takes a changeset and an action and “applies”, returning either an ok-tuple or an error-tuple, similar to Repo.insert/1.

iex(6)> sign_up = %{ sign_up | password_confirmation: "test"}
%{
  email: "foo@bar.com",
  name: "Foo Bar",
  password: "test",
  password_confirmation: "test",
  phone_number: "15555551234"
}
iex(7)> SignUp.changeset(%SignUp{}, sign_up) |>
...(7)> Ecto.Changeset.apply_action(:insert)
{:ok,
 %Knocker.Accounts.SignUp{
   email: "foo@bar.com",
   id: nil,
   name: "Foo Bar",
   password: "test",
   password_confirmation: "test",
   password_hash: nil,
   phone_number: "15555551234"
 }}

But what of the password_hash? I’m using the comeonin libarary to handle hashing the libaray, using a couple simple functions to add the hashed password to the changeset:

  defp put_pass_hash(%Ecto.Changeset{
    valid?: true,
    changes: %{password: password}
    } = changeset) do
    changeset
    |> change(Bcrypt.add_hash(password))
    |> change(%{password_confirmation: nil})
  end

  defp put_pass_hash(changeset), do: changeset

The put_pass_hash/1 function matches on a valid changeset that contains a password in the list of changes, and uses [add_hash/1][8] function to create the hashed password and sets the password in the changeset to nil. I must manually nil out the password_confirmation field. If the changeset is invalid or there is no password, then I return the changeset without modification.

Once I add it to the module and changeset/2, generating a valid changeset also changes a given password and replaces it with a newly hashed password.

defmodule Knocker.Accounts.SignUp do
  use Ecto.Schema

  embedded_schema do
    field(:email, :string)
    field(:name, :string)
    field(:phone_number, :string)
    field(:password, :string, virtual: true)
    field(:password_confirmation, :string, virtual: true)
    field(:password_hash)
  end

  def changeset(%SignUp{} = sign_up, attrs) do
    sign_up
    |> cast(attrs, [:email, :name, :password, :password_confirmation, :phone_number])
    |> validate_required([:email, :name, :password, :password_confirmation, :phone_number])
    |> validate_confirmation(:password)
-   # |> put_pass_hash
+   |> put_pass_hash
  end

+ defp put_pass_hash(%Ecto.Changeset{
+     valid?: true,
+     changes: %{password: password}
+   } = changeset) do
+   changeset
+   |> change(Bcrypt.add_hash(password))
+   |> change(%{password_confirmation: nil})
+ end

+ defp put_pass_hash(changeset), do: changeset

end

And here the hash is generated!

iex(1)> SignUp.changeset(%SignUp{}, sign_up) |>
...(1)> Ecto.Changeset.apply_action(:insert)
{:ok,
 %Knocker.Accounts.SignUp{
   email: "foo@bar.com",
   id: nil,
   name: "Foo Bar",
   password: nil,
   password_confirmation: nil,
   password_hash: "$2b$12$irnK/3/DjNRTSGXmZSaBKegNOTREALHASHJvQzcqUBCJhDmSTRyfu",
   phone_number: "15555551234"
 }}

Now we have a validated struct generated from our form inputs, which can now be fed into the User module’s changeset/2.

The sign-up controller, then, is simply

  def create(conn, %{"user" => user_params}) do
    with {:ok, %User{} = user} <- create_user(user_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", user_path(conn, :show, user))
      |> render("show.json", user: user)
    end
  end

  defp create_user(attrs \\ %{}) do
    case create_sign_up(attrs) do
      {:ok, user} ->
        %User{}
        |> User.changeset(Map.from_struct(user))
        |> Repo.insert()

      error ->
        error
    end
  end

  defp create_sign_up(attrs \\ %{}) do
    %SignUp{}
    |> SignUp.changeset(attrs)
    |> Changeset.apply_action(:insert)
  end

Provided our sign-up form is valid, a new user is recorded in the database. If not, an error is provided to the client.

Sign-In Controller and Schema

Another endpoint is the sign-in endpoint, where an e-mail and password is provided and a token is returned.

defmodule Knocker.Accounts.SignIn do
  use Ecto.Schema
  import Ecto.Changeset
  alias Comeonin.Bcrypt
  alias Knocker.Accounts.SignIn

  embedded_schema do
    field(:email, :string)
    field(:password, :string)
  end

  def changeset(%SignIn{} = sign_in, attrs) do
    sign_in
    |> cast(attrs, [:email, :password])
    |> validate_required([:email, :password])
  end
end

This schema is simple, it only validates the presence of an email and a password. The controller, then, is:

def create(conn, %{"user" => user_params}) do
  with {:ok, %SignIn{} = sign_in} <- create_sign_in(user_params),
        %User{} = user <- get_user_by_email!(sign_in.email),
        {:ok, _} <- sign_in_user(user, sign_in.password),
        {:ok, jwt, _} <- Guardian.encode_and_sign(user) do
    conn
    |> Plug.sign_in(user)
    |> put_status(:ok)
    |> put_resp_header("authorization", "Bearer #{jwt}")
    |> render("show.json", sign_in: jwt)
  else
    _ -> {:error, :unauthorized}
  end
end

defp create_sign_in(attrs \\ %{}) do
  %SignIn{}
  |> SignIn.changeset(attrs)
  |> Ecto.Changeset.apply_action(:insert)
end

defp get_user_by_email!(email) do
  User
  |> where([u], u.email == ^email)
  |> select([u], u)
  |> limit(1)
  |> Repo.one!()
end

defp sign_in_user(user, password) do
  SignIn.check_pw(user, password)
end

defp check_pw(user, password), do: Bcrypt.check_pass(user, password)

Once again, I use Ecto.Changeset.apply_action/2 to ensure the given sign in data is valid, and then fetch a user by email and ensure the password matches.

If all is :ok, I then generate and return a token. If not, I return :unauthorized.

Conclusion

The use of Ecto changesets to validate more than just persisted models helps in simplifying validation code of incoming data sets for API calls (and also forms), and, because I am using Ecto, I have access to the entire suite of model validations bundled into Ecto and write my own re-usable Ecto validation functions in the event that things get more complicated.

I ❤ feedback. Let me know what you think of this article on Twitter @afontaine_ca