Logging In

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).

Authentication

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.

Login API

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 }
  }
 }
images/chp.auth/graphiql-1.png

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.2.225