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