July 15, 2016

How to Detect Suspicious Logins in an Elixir / Phoenix Web App

Phoenix is a relatively new web application framework for the Elixir programming language, which is itself runs on the Erlang virtual machine (BEAM).

Elixir and Phoenix are interesting because they together bring functional, concurrent programming to web application development. This means that web application developers can get the distributed, fault-tolerant benefits of Erlang in a modern "Ruby-like" language and framework that takes many cues from Ruby on Rails.

You can learn more about Elixir and Phoenix here:

In this tutorial we're going to add some log in security to a boilerplate example Phoenix application. It'll help if you've seen some Elixir (or even Ruby) code before, but if you're familiar with programming you should be able to follow along.

Getting started

We'll start with a example application, PhoenixGuardian. This is a basic app that uses popular Elixir modules Überauth and Guardian to implement authentication.

Let's start by cloning the project from Github, installing dependencies, and starting the web app.

# Clone the repo
cd ~/Desktop  
git clone [email protected]:hassox/phoenix_guardian.git  
cd phoenix_guardian

# Install some basics - skip this if you've installed these already!
brew install elixir npm postgresql

# Install dependencies
mix deps.get && npm install

# Create and migrate the database
mix ecto.create  
mix ecto.migrate

# Start the phoenix server
mix phoenix.server  

You should now be able to view the web app and create an account at http://localhost:4000.

Next we're going to modify the web app to send events to ThisData so that we can track authentication activity and optionally alert our users when someone accesses their account maliciously.

Start by opening the project in your favourite text editor, like Sublime Text 3!

I'm going to skip over some details of the ThisData API - you might want to review our documentation over at http://help.thisdata.com/docs/ before getting stuck in!

Adding dependencies

We need to add some dependencies to our application in order to make HTTP requests to the ThisData API. HTTPoison is an HTTP library, which will also install hackney, which it is based on.

Open mix.exs from the project root directory, then edit the defp deps function to add the following to the list of dependencies:

{:httpoison, "~> 0.9.0"}

The function should end up looking like this:

# Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps do
    [{:ex_machina, "~>0.6", only: [:dev, :test]},
     # [snipped]
     {:cowboy, "~> 1.0"},
     {:httpoison, "~> 0.9.0"}]
  end

Then we need to update the def applications function (above deps) to tell our app we're depending on HTTPoison. Edit the mix.exs file again and the following at the end of the list in def applications:

:httpoison

All right, now that we've added our dependencies, we need to install them and restart the Phoenix server. Quit the server with ^C and then a to abort. Then type:

mix deps.get  
mix phoenix.server  

Add a ThisData API client

We need to implement a basic API client for the ThisData API so we can send event data to it over HTTP. Copy and paste the following into a new file at web/this_data/api.ex. Note that everything inside the web/ folder is live-reloaded in the Phoenix development environment so we shouldn't need to restart our Phoenix server again (for a while).

# web/this_data/api.ex

defmodule ThisData.Api do  
  require Logger
  use HTTPoison.Base

  # Define our API endpoint host
  @host "https://api.thisdata.com"

  # Automatically prepend the host above onto our URL when we call the API
  # client
  def process_url(url) do
    @host <> url
  end

  # Get the ThisData API key from environment variables
  def api_key do
    System.get_env("THIS_DATA_API_KEY")
  end

  # Encode the request body to JSON format
  def process_request_body(body) do
    body
    |> Poison.encode!
  end

  # Decode the response body from JSON format.
  #
  # Note that because the ThisData API response is a `null` JSON response, we
  # need to just return `nil` in this situation rather than `Poison`'s error
  # response
  def process_response_body(body) do
    case Poison.decode(body) do
      {:ok, body} ->
        body
      {:error, :invalid} ->
        nil
    end
  end

  # Send the event to ThisData.
  #
  # The body is the request body specified here:
  # http://help.thisdata.com/docs/apiv1events
  #
  # This method will return `:ok` or `:error` depending on success and log what
  # is happening
  def send_event(body) do

    # Only proceed if the API key is present (see `api_key` above)
    if is_binary(api_key) do

      Logger.debug "making ThisData API request with body: " <> Kernel.inspect(body)

      # Start the API client Process. See 
      # http://elixir-lang.org/getting-started/processes.html for more info.
      ThisData.Api.start

      # Use the text/json content type
      headers = %{"Content-Type" => "text/json"}

      # Make the API request and handle different response conditions
      case ThisData.Api.post("/v1/events.json?api_key=#{api_key}", body, headers) do
        {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
          Logger.debug "ThisData API request successful: " <> Kernel.inspect(body)
          :ok
        {:error, %HTTPoison.Error{reason: reason}} ->
          Logger.warn "ThisData API request failed for " <> Kernel.inspect(reason)
          :error
      end
    else  
      Logger.warn "THIS_DATA_API_KEY not configured, skipping API request"
    end
  end

end  

Next let's add a module that has some functions that we can call directly from the Phoenix UserController and that build the request body for ThisData.Api.

Create a new file: web/this_data/this_data.ex, and copy this following:

# web/this_data/this_data.ex

defmodule ThisData do  
  require Logger

  # Track a log in event
  def track_log_in(conn, user) do
    Logger.debug "tracking log in"
    conn
    |> track("log-in", user)
  end

  # Track a log out event
  def track_log_out(conn, user) do
    Logger.debug "tracking log out"
    conn
    |> track("log-out", user)
  end

  # Track a log in denied event
  def track_log_in_denied(conn, auth) do
    Logger.debug "track log in denied"
    conn
    |> track("log-in-denied", nil, auth)
  end

  # Build a request body and send a track request via ThisData.Api
  #
  # This function takes a connection (conn), verb, and optionally user and auth.
  # The auth item contains credentials we can send where we don't know who the
  # user is (because they haven't successfully logged in).
  defp track(conn, verb, user \\ nil, auth \\ nil) do

    # Exact the user agent from the connection headers
    user_agents = Plug.Conn.get_req_header(conn, "user-agent")
    user_agent = List.first(user_agents)

    # Get the remote IP from the X-Forwarded-For header if present, so this
    # works as expected when behind a load balancer
    remote_ips = Plug.Conn.get_req_header(conn, "x-forwarded-for")
    remote_ip = List.first(remote_ips)

    # If there was nothing in X-Forarded-For, use the remote IP directly
    unless remote_ip do
      # Extract the remote IP from the connection
      remote_ip_as_tuple = conn.remote_ip

      # The remote IP is a tuple like `{127, 0, 0, 1}`, so we need join it into
      # a string for the API. Note that this works for IPv4 - IPv6 support is
      # exercise for the reader!
      remote_ip = Enum.join(Tuple.to_list(remote_ip_as_tuple), ".")
    end

    # Default values for our API attributes
    user_id = "anonymous"
    user_email = nil
    user_name = nil

    # If there is a user item, populate the API attributes with its details,
    # otherwise if there is an auth item grab the email from it
    cond do
      user ->
        user_id = user.id
        user_name = user.name
        user_email = user.email
      auth ->
        user_email = auth.info.email
    end

    # Construct the request body
    body = %{
      verb:       verb,
      ip:         remote_ip,
      user_agent: user_agent,
      user:       %{
        id:    user_id,
        email: user_email,
        name:  user_name
      }
    }

    # Send the event via ThisData.Api
    result = ThisData.Api.send_event(body)
    Logger.debug "Result: " <> Kernel.inspect(result)

    # Return the connection again, since we'll be calling this method via an
    # Elixir pipe operator - see 
    # https://elixirschool.com/lessons/basics/pipe-operator/
    conn
  end

end  

Add hooks in AuthController

So far we've added a ThisData module which provides a tidy API for us to track events from, and a ThisData.Api module which handles API communication over HTTP (making use of HTTPoison).

Next we'll edit the AuthController at web/controllers/auth_controller.ex and call these methods during authentication.

Open web/controller/auth_controller.ex and add the ThisData.track.. calls to the second def callback method like so:

def callback(%Plug.Conn{assigns: %{ueberauth_auth: auth}} = conn, _params, current_user, _claims) do  
    case UserFromAuth.get_or_insert(auth, current_user, Repo) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "Signed in as #{user.name}")
        |> Guardian.Plug.sign_in(user, :access, perms: %{default: Guardian.Permissions.max})
        |> ThisData.track_log_in(user)
        |> redirect(to: private_page_path(conn, :index))
      {:error, _reason} ->
        conn
        |> ThisData.track_log_in_denied(auth)
        |> put_flash(:error, "Could not authenticate. Error: #{_reason}")
        |> render("login.html", current_user: current_user, current_auths: auths(current_user))
    end
  end

There are two interesting tidbits here! Firstly notice that although our functions ThisData.track_log_in and ThisData.track_log_in_denied both expect the first argument to be a connection, we haven't passed one in. This is because we're calling the functions via a pipe operator. In pipes the first argument is automatically the originating item (the head of the pipe, conn).

Secondly there are two def callback functions. This is because in Elixir a function definition includes it's arity (arguments). So you can have multiple functions named the same, so long as their arguments are (unambiguously) different!

Next let's edit the def logout function and look into our ThisData module as well, to track log outs:

def logout(conn, _params, current_user, _claims) do  
    if current_user do
      conn
      # This clears the whole session.
      # We could use sign_out(:default) to just revoke this token
      # but I prefer to clear out the session. This means that because we
      # use tokens in two locations - :default and :admin - we need to load it (see above)
      |> Guardian.Plug.sign_out
      |> ThisData.track_log_out(current_user)
      |> put_flash(:info, "Signed out")
      |> redirect(to: "/")
    else
      conn
      |> put_flash(:info, "Not logged in")
      |> redirect(to: "/")
    end
  end

And that's it! Finally let's start the Phoenix server with your API key:

THIS_DATA_API_KEY=your_key_goes_here mix phoenx.server  

(Find out how to get your API key here: http://help.thisdata.com/docs/authentication-api-key)

Now in the Phoenix server output (see the Terminal you've got mix phoenix.server running) should including debug messages like this when a log in is denied:

[info] POST /auth/identity/callback
[debug] Processing by PhoenixGuardian.AuthController.callback/2
  Parameters: %{"_csrf_token" => "xxxxxxx==", "_utf8" => "✓", "email" => "wrong", "identity" => "identity", "password" => "[FILTERED]"}
  Pipelines: [:browser, :browser_auth]
[debug] QUERY OK db=2.7ms queue=0.1ms
SELECT a0."id", a0."provider", a0."uid", a0."token", a0."refresh_token", a0."expires_at", a0."user_id", a0."inserted_at", a0."updated_at" FROM "authorizations" AS a0 WHERE ((a0."uid" = $1) AND (a0."provider" = $2)) ["a", "identity"]  
[debug] track sign in denied
[debug] making ThisData API request
[debug] ThisData API request successful: nil
[debug] Result: :ok
[info] Sent 200 in 45ms

Now if you log in to ThisData your security dashboard gives you a great overview of authentication activity within your Phoenix app

To check out the completed project, check out our fork of PhoenixGuardian here: https://github.com/thisdata/phoenix_guardian/tree/thisdata

Next steps

While the PhoenixGuardian project doesn't include a password reset feature, it would be straight forward to add some monitoring for this by editing the ThisData module and adding a new function to track_password_reset.

I leave this as an exercise for the reader.

Let us know how you get on!

More information

For more information about ThisData and ThisData API, check out:

More information about Elixir, Phoenix, and the libraries we used:

YOU MAY ALSO BE INTERESTED IN

The future of authentication

Today I’m excited to announce a deal that we have been working on for the past few months and how that will impact the future of contextual ...

Introducing custom security rules

For the past few years we’ve been working hard to create a plug and play adaptive risk engine. We designed our core service using a mix of b ...