Validating API Keys

We're moving along at a great pace so let's try to keep it up and implement an API key authentication mechanism so that all of our work here was not in vain! We'll use the username as the username for the user making API requests and the API key as the password for an HTTP Basic Authentication scheme. This is generally a good method for authenticating your API and will serve our purposes nicely.

We'll start off by implementing a new plug that will be responsible for handling our API key checks. We'll call this one VerifyApiKey to keep things consistent with our other custom plug. Create lib/vocial_web/verify_api_key.ex and we'll start building this out:

defmodule VocialWeb.VerifyApiKey do
def init(opts), do: opts

def call(conn, _opts) do
conn
end
end

We'll also need to hook it up to our API pipeline in the router. Open up lib/vocial_web/router.ex and in it, we're going to add a new call to plug that will pass in the preceding new module that we started to write:

  pipeline :api do
plug :accepts, ["json"]
plug VocialWeb.VerifyApiKey
end

Right now we're not doing anything particularly interesting; this is just our initial Plug template starter. Let's make this a little more interesting, though. Since we don't have a means of checking an API key yet, let's just make this always return a message saying that the API key was invalid!

def call(conn, _opts) do
conn
|> put_status(401)
|> render(VocialWeb.ErrorView, "invalid_api_key.json")
|> halt()
end

To make this work, we'll also need to import our put_status/2, render/3, and halt/1 calls. Up at the top of the file, add our imports:

  import Plug.Conn, only: [halt: 1, put_status: 2]
import Phoenix.Controller, only: [render: 3]

This won't work until we also edit the ErrorView to include an invalid_api_key.json render clause. Open up lib/vocial_web/views/error_view.ex and add the following function:

  def render("invalid_api_key.json", _assigns) do
%{ message: "Invalid API Key" }
end

The beauty of Phoenix for writing an API is that all of the difficult serialization logic is already handled for us! We just need to return out the appropriate data structure and the rest is handled for us!  Let's run our API request again and we should be able to validate that we're now receiving an error message!

Let's expand this functionality a little bit more by making it actually implement an API key check! We'll need to do a couple of things in order to verify the API key:

  1. Fetch the Authorization header out of the conn (this is a header coming from the client making the API request)
  2. Decode the Authorization header from Base64 into a regular string
  3. Split the string apart into a username and API key, separately
  4. Verify the username and API key passed in

Let's implement each of these functions in turn. We'll start with fetching the authorization header out of the passed-in conn. We then convert the list of headers into a map to make fetching values out of them simpler, and fetch the "authorization" key:

  def fetch_authorization_header(conn) do
conn.req_headers
|> Enum.into(%{})
|> Map.fetch("authorization")
end

Next, we'll implement the function that will be responsible for decoding the Authorization header from Base64 into plain text. I'm going to step through this piece-by-piece to make it a little easier for me to explain:

The first piece is our function signature. We're expecting this to be a function that will just take in the fetched "authorization" header from before:

def decode_authorization_header(auth_header) do
# ...
end

Next, we'll split apart the auth header into two parts. Generally an authorization header looks something like this: Basic AbCd1Efgh54319  so we'll need to split the auth header apart by splitting on spaces. This will give us the type of authentication (the first string) and the encoded authorization information:

[type, key] = String.split(auth_header, " ")

Next, we'll convert the type to lowercase to avoid any sort of weird issues where each client sends the authorization information differently each time, and we'll do a case statement on that to make sure we're only ever dealing with basic auth:

case String.downcase(type) do
# ...
end

Finally, as we mentioned, we want to decode the authorization information if it is indeed basic auth, so we'll include that as our first clause and an error message as our second clause:

      "basic" -> Base.decode64(key)
_ -> {:error, "Invalid Authorization Header Format"}

The fully-finished function should look like this:

  def decode_authorization_header(auth_header) do
[type, key] = String.split(auth_header, " ")
case String.downcase(type) do
"basic" -> Base.decode64(key)
_ -> {:error, "Invalid Authorization Header Format"}
end
end

We'll also need a function in our Accounts Context that will be responsible for verifying the username/API key. Since they're both stored as plain text values in our users' table, this becomes a simple check using Repo.get_by/2. In lib/vocial/accounts/accounts.ex:

  def verify_api_key(username, api_key) do
case Repo.get_by(User, username: username, api_key: api_key) do
nil -> false
_user -> true
end
end

Finally, we can go back and finish writing our plug out. First, at the top of the plug, add an alias for the Accounts Context:

alias Vocial.Accounts

Then we'll refactor a little bit to keep our functions small and purpose-specific. We'll start by writing a function to handle dealing with the conn when the API key check fails:

  def invalid_api_key(conn) do
conn
|> put_status(401)
|> render(VocialWeb.ErrorView, "invalid_api_key.json")
|> halt()
end

We'll also need a check for verifying the API key itself is valid. This is a long function overall, but nothing terribly complicated. This is essentially a pipeline, where we take the request headers, fetch the authorization header out of those, decode the authorization header from Base64 encoding, then split the authorization information. In our case, we expect the user to send their authorization information by using their username as the username for HTTP Basic auth and their API key as the password:

  def is_valid_api_key?(conn) do
with {:ok, header} <- fetch_authorization_header(conn),
{:ok, decoded_header} <- decode_authorization_header(header),
[username, api_key] = String.split(decoded_header, ":")
do
Accounts.verify_api_key(username, api_key)
else
_ -> false
end
end

Finally, we'll need two functions to handle what to do with the actual conn. One for when the key is valid, and one for when the key is invalid:

  def handle_conn(true, conn), do: conn
def handle_conn(_, conn), do: invalid_api_key(conn)

That's it! Now we can go back to our call/2 function implementation and refactor it:

  def call(conn, _opts) do
is_valid_api_key?(conn)
|> handle_conn(conn)
end

That's it! We're left with a very clean call/2 function implementation since we split apart our functions and kept the functionality of each super lean!

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

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