Online ordering is all the rage right now, as customers look to beat the lines by placing an order online and then picking it up shortly afterward. The (theoretical) mobile team has been hard at work on a mobile application that customers can use to place orders from the comfort of their homes, so we need to provide a way to do this securely.
So far, when we’ve been responding to API requests, we haven’t been concerned with who is making those requests; we’ve been focused on how to deal with the data itself. Both menu updates and the orders themselves have come from within the restaurant, so we could just accept whatever it sent us. If we’re going to accept orders from the customers themselves, however, not only do we need to keep track of who has ordered what, but we also need to give each customer the ability to view and subscribe to their orders (and no one else’s).
Tracking customers also entails tracking employees, because we need a way to permit employees to carry out actions that are forbidden for customers, like editing the menu or completing an order. In fact, most of the operations in the system at the moment ought only to be carried out by employees.
The first step then is being able to identify whether someone is an employee or a customer. From there, we’ll see how we can use this information to perform authentication and authorization checks within our schema.
Our goal is to support a simple mutation like the following:
| mutation Login($email: String!, $password: String!) { |
| login(role: EMPLOYEE, email: $email, password: $password) { |
| token |
| } |
| } |
This should return an API token valid for this particular employee if the supplied email address and password are correct. The response from our API should look something like this:
| { |
| "data": { |
| "login": { |
| "token": "EMPLOYEE-TOKEN-HERE" |
| } |
| } |
| } |
To do this, though, we need to make a few additions to our database and Ecto schemas in order to have the data on hand. We’ll get those changes in, and then we’ll build out the code necessary to integrate the data with our API.
There are many different ways to model users, but we don’t need something particularly complicated for our use case. We’re going to use a single users table that will hold the user’s name, email address, and password. We’ll also have a role column to indicate whether they’re an employee or a customer.
Our first order of business is to create the schema and migration we need to manage these users. We can use a Phoenix generator to get some of the basics going:
| $ mix phx.gen.schema Accounts.User users |
| name:string |
| email:string |
| password:string |
| role:string |
Make a small change to the generated migration to add a unique index on the user email and role:
| defmodule PlateSlate.Repo.Migrations.CreateUsers do |
| use Ecto.Migration |
| |
| def change do |
| create table(:users) do |
| add :name, :string |
| add :email, :string |
| add :password, :string |
| add :role, :string |
| |
| timestamps() |
| end |
| |
» | create unique_index(:users, [:email, :role]) |
| |
| end |
| end |
There’s a useful package called :comeonin_ecto_password that we’re going to use to hash the password for us. Let’s add it with a compatible hash algorithm dependency to our mix.exs file:
| defp deps do |
| [ |
» | {:comeonin_ecto_password, "~> 2.1"}, |
» | {:pbkdf2_elixir, "~> 0.12.0"}, |
| # Other deps |
| ] |
| end |
Here’s all we need to make a small tweak to the User schema to set that up:
| schema "users" do |
| field :email, :string |
| field :name, :string |
» | field :password, Comeonin.Ecto.Password |
| field :role, :string |
| |
| timestamps() |
| end |
Additionally, we’re going to put a couple of extra columns on the orders table that we can use to relate a given order to the customer who ordered it.
| $ mix ecto.gen.migration AddCustomerToOrders |
| defmodule PlateSlate.Repo.Migrations.AddCustomerToOrders do |
| use Ecto.Migration |
| |
| def change do |
| alter table(:orders) do |
| add :customer_id, references(:users) |
| end |
| end |
| end |
After we run our migrations, we’ll be all set up:
| $ mix ecto.migrate |
| Compiling 1 file (.ex) |
| Generated plate_slate app |
| [info] == Running Migrations.CreateUsers.change/0 forward |
| [info] create table users |
| [info] create index users_email_role_index |
| [info] == Migrated in 0.0s |
| [info] == Running Migrations.AddCustomerToOrders.change/0 forward |
| [info] == Migrated in 0.0s |
The column we’ve added to the orders table needs a corresponding line in the order schema module, and the addition of the :customer_id to the changeset function’s cast list:
| defmodule PlateSlate.Repo.Migrations.AddCustomerToOrders do |
| use Ecto.Migration |
| |
| def change do |
| alter table(:orders) do |
| add :customer_id, references(:users) |
| end |
| end |
| end |
We can use the new "users" table to build out a basic PlateSlate.Accounts module and authenticate/3 function:
| defmodule PlateSlate.Accounts do |
| @moduledoc """ |
| The Accounts context. |
| """ |
| import Ecto.Query, warn: false |
| alias PlateSlate.Repo |
| alias Comeonin.Ecto.Password |
| |
| alias PlateSlate.Accounts.User |
| |
| def authenticate(role, email, password) do |
| user = Repo.get_by(User, role: to_string(role), email: email) |
| |
| with %{password: digest} <- user, |
| true <- Password.valid?(password, digest) do |
| {:ok, user} |
| else |
| _ -> :error |
| end |
| end |
| end |
We’re using a pretty simple authentication mechanism here: just role, email, and password. Our function looks up a customer by role and email address, and then the password the customer supplies is compared against the stored password. If the email belongs to a user, and if the password is valid, our function here will return {:ok, user}. You can find a variety of authentication and user management systems on Hex,[26] and you may well find that one of those suits your particular needs very well. The way you’d integrate this with Absinthe will be almost exactly the same in each case.
Before we integrate authentication into PlateSlate’s API, fire up iex -S mix and get a feel for using authenticate/3. Here’s an example of creating employee and customer users and then successfully authenticating them:
| iex(1)> alias PlateSlate.Accounts |
| iex(2)> %Accounts.User{} |> |
| Accounts.User.changeset(%{role: "employee", name: "Becca Wilson", |
| email: "[email protected]", password: "abc123"}) |> PlateSlate.Repo.insert! |
| #=> %Accounts.User{...} |
| |
| iex(3)> %Accounts.User{} |> |
| Accounts.User.changeset(%{role: "customer", name: "Joe Hubert", |
| email: "[email protected]", password: "abc123"}) |> PlateSlate.Repo.insert! |
| #=> %Accounts.User{...} |
| |
| iex(4)> Accounts.authenticate("employee", "[email protected]", "abc123") |
| {:ok, |
| %Accounts.User{ |
| email: "[email protected]", id: 1, inserted_at: ~N[2017-08-28 18:14:15.785375], |
| name: "Becca Wilson", |
| password: "$pbkdf2-sha512$16...", |
| role: "employee", updated_at: ~N[2017-08-28 18:14:15.786666]}} |
If you try to log in with either an invalid email/password or with the wrong role, you’ll get an :error atom as the result:
| iex(5)> alias PlateSlate.Accounts |
| iex(6)> Accounts.authenticate("customer", "[email protected]", "abc123") |
| :error |
| iex(7)> Accounts.authenticate("employee", "[email protected]", "123") |
| :error |
| iex(8)> Accounts.authenticate("employee", "[email protected]", "abc123") |
| :error |
While simple, this user modeling accomplishes a lot. The role column lets us distinguish employees from customers, and this makes it easy to write code that handles both as we move forward.
With the underlying database work all set, the next task is to define the mutation for your API. Head over to your Absinthe schema and add a :login mutation field to the root mutation type:
| mutation do |
| |
| field :login, :session do |
| arg :email, non_null(:string) |
| arg :password, non_null(:string) |
| arg :role, non_null(:role) |
| resolve &Resolvers.Accounts.login/3 |
| end |
| # Other mutation fields |
| end |
The mutation requires an email address, password, and role, and it returns a :session type. We’ll be creating a new type module to house this type and the others like it:
| defmodule PlateSlateWeb.Schema.AccountsTypes do |
| use Absinthe.Schema.Notation |
| |
| object :session do |
| field :token, :string |
| field :user, :user |
| end |
| |
| enum :role do |
| value :employee |
| value :customer |
| end |
| interface :user do |
| field :email, :string |
| field :name, :string |
| resolve_type fn |
| %{role: "employee"}, _ -> :employee |
| %{role: "customer"}, _ -> :customer |
| end |
| end |
| |
| object :employee do |
| interface :user |
| field :email, :string |
| field :name, :string |
| end |
| |
| object :customer do |
| interface :user |
| field :email, :string |
| field :name, :string |
| field :orders, list_of(:order) |
| end |
| end |
There are a couple of interesting types here. At the top, we’ve got the :session object returned from the :login mutation, which contains an API token and a user field. This user field is an interface, which you learned about back in Chapter 4, Adding Flexibility. Both employee and customer objects have email and name fields. However, we still want to keep them as separate objects because, as our API grows, there will be fields that only apply to one but not the other. In a bit, we’ll be filling out the orders field on the customer, but this field doesn’t make much sense on an employee.
The resolution function for the login field is Resolvers.Accounts.login/3. We’ll add it in a new resolver module:
| defmodule PlateSlateWeb.Resolvers.Accounts do |
| alias PlateSlate.Accounts |
| |
| def login(_, %{email: email, password: password, role: role}, _) do |
| case Accounts.authenticate(role, email, password) do |
| {:ok, user} -> |
| token = PlateSlateWeb.Authentication.sign(%{ |
| role: role, id: user.id |
| }) |
| {:ok, %{token: token, user: user}} |
| _ -> |
| {:error, "incorrect email or password"} |
| end |
| end |
| end |
Here we’re using the Accounts.authenticate/3 function we built earlier, and if it’s successful, creating a token using the PlateSlateWeb.Authentication module. This module is really just a small wrapper about the token generation abilities we get from Phoenix.Token.
| defmodule PlateSlateWeb.Authentication do |
| @user_salt "user salt" |
| |
| def sign(data) do |
| Phoenix.Token.sign(PlateSlateWeb.Endpoint, @user_salt, data) |
| end |
| |
| def verify(token) do |
| Phoenix.Token.verify(PlateSlateWeb.Endpoint, @user_salt, token, [ |
| max_age: 365 * 24 * 3600 |
| ]) |
| end |
| |
| end |
The token encodes information about the type of session, as well as who the session belongs to, by including the employee.id. We’ll need this information to know what role (customers or employees) to use when we want to look up the user record later.
Now we’re ready to write some basic tests to ensure our mutation is built and behaves correctly. The first thing we’ll do is create a small helper module for generating users so that we can have some on hand in this and any future tests:
| defmodule Factory do |
| def create_user(role) do |
| int = :erlang.unique_integer([:positive, :monotonic]) |
| params = %{ |
| name: "Person #{int}", |
| email: "fake-#{int}@example.com", |
| password: "super-secret", |
| role: role |
| } |
| |
| %PlateSlate.Accounts.User{} |
| |> PlateSlate.Accounts.User.changeset(params) |
| |> PlateSlate.Repo.insert! |
| end |
| end |
With that out of the way, we can look at the login test itself:
| defmodule PlateSlateWeb.Schema.Mutation.LoginEmployeeTest do |
| use PlateSlateWeb.ConnCase, async: true |
| |
| @query """ |
| mutation ($email: String!) { |
| login(role: EMPLOYEE, email:$email,password:"super-secret") { |
| token |
| user { name } |
| } |
| } |
| """ |
| test "creating an employee session" do |
| user = Factory.create_user("employee") |
| response = post(build_conn(), "/api", %{ |
| query: @query, |
| variables: %{"email" => user.email} |
| }) |
| |
| assert %{"data" => %{ "login" => %{ |
| "token" => token, |
| "user" => user_data |
| }}} = json_response(response, 200) |
| |
| assert %{"name" => user.name} == user_data |
| assert {:ok, %{role: :employee, id: user.id}} == |
| PlateSlateWeb.Authentication.verify(token) |
| end |
| end |
We use the employee’s information in our test to ensure that, given the correct credentials, the correct token is returned from our :login mutation. Let’s run the test:
| $ mix test test/plate_slate_web/schema/mutation/login_test.exs |
| . |
| |
| Finished in 0.1 seconds |
| 1 test, 0 failures |
We can also see this working in GraphiQL because of the user we created in IEx earlier, so let’s give that a shot by starting the server:
| mix phx.server |
| mutation { |
| login(role: CUSTOMER, email:"[email protected]",password:"abc123") { |
| token |
| user { name __typename } |
| } |
| } |
It worked! As shown in the figure, we got back an auth token and some information about the employee we just authenticated. Now we just take that authentication token and…do what with it? Is it something that should get passed to future GraphQL fields?
This is the next thing we need to figure out.
18.118.2.225