Protecting elixir apps with geoip

November 18, 2019

This is a common case these days to provide your service only to specific geo locations. It’s easy to bypass it but the law is not following changes in the tech world. Anyway, let’s try to implement a service that is checking client’s IP and allows to get in if the IP is on our whitelist.

I’ve chosen ipdata.co (https://ipdata.co/) as staring an integration is very simple and you can register for free. We will need just HTTPoison package added to our Phoenix deps.

After registration you’ll receive an API KEY, let’s put to our config (I named our project “Demo”) or actually let’s use config to read that key from our ENV.

config :demo, Demo.Services.GeoIp,
  api_key: System.get_env("IP_DATA_API_KEY"),
  url: "https://api.ipdata.co"

Create a new file in your project: lib\demo\services\geo_ip.ex and add the following functions:

defmodule Demo.Services.GeoIp do
  @moduledoc """
  Get GEO Ip data by ip address
  """

  # call the API and check the location for given ip
  def get_location(ip) do
    case HTTPoison.get("#{url()}/#{ip}?api-key=#{api_key()}") do
      {:ok, res} -> {:ok, Jason.decode!(res.body)}
      {:error, reason} -> {:error, reason}
    end
  end

  # get API KEY from config
  defp api_key do
    Application.get_env(:demo, Demo.Services.GeoIp)[:api_key]
  end

 # get API url from config
  defp url do
    Application.get_env(:demo, Demo.Services.GeoIp)[:url]
  end
end

After building the interface to ipdate.co, we can implement the actual Plug that we’ll use to protect connections to the server.

defmodule DemoWeb.Plugs.WhitelistLocation do
  @moduledoc """
  Use to whitelist locales before accepting the request
  """
  import Plug.Conn

  alias Demo.Services.GeoIp

  # whitelisted regions
  @whitelist ["NY", "DS"]

  def init(_opts), do: nil

  def call(conn, _opts) do
    case get_session(conn, :location) do
      nil ->
        region = get_location(conn.remote_ip)

        conn
        |> put_session(:location, region)
        |> check_whitelist(region)

      location ->
        check_whitelist(conn, location)
    end
  end

  defp check_whitelist(conn, location) do
    if Enum.member?(@whitelist, location) do
      conn
    else
      conn
      |> put_status(403)
      |> send_resp(403, "Forbidden")
      |> halt()
    end
  end

  defp get_location(ip) do
    string_ip = to_string(:inet_parse.ntoa(ip))

    case GeoIp.get_location(string_ip) do
      {:ok, %{"region_code" => region}} -> region
      # Set availability for localhost
      {:ok, %{"message" => "127.0.0.1 is a private IP address"}} -> "NY"
      {:error, _} -> nil
    end
  end
end

The last step is to plug the plug in :)

  pipeline :browser do
    ...
    plug DemoWeb.Plugs.WhitelistLocation
    ...
  end