Basic HTTP authentication in Elixir/Phoenix

Let’s look on what HTTP Basic authentication is and how to implement and test the HTTP Basic authentication in a Phoenix web application.

Basic is one of the authentication schemes we can use to authenticate access on the web (other is for example a Bearer scheme for OAuth 2.0 tokens). Using the Basic scheme is very simple. If our server responds with 401 Unauthorized response including WWW-Authenticate response header with a Basic challenge as follows:

WWW-Authenticate: Basic realm="Access to the application"

the browser can automatically ask the user for login credentials (login and password). The browser then connects the user and password together (separated by colon) and uses Base64 encoding to create a single string that is provided in the request Authorization header:

Authorization: Basic am1lbm9oZXNsbw==\n

One important thing to realize is that this header will now be sent by browser automatically with subsequent requests as long as they remember it (until browser restarts).

Usually, we don’t dig the details of this challenge as authentication libraries or frameworks do this for us. In Elixir world we can reach for BasicAuth plug:

# mix.exs
...
{:basic_auth, "~> 2.2.2"}
...

Once added, fetched, and compiled we can use it as any other plug in the Phoenix router:

...
pipeline :basic_auth do
  plug BasicAuth,
    callback: &MyAppWeb.Protected.Authentication.authenticate/3,
    custom_response: &MyAppWeb.Protected.Helpers.unauthorized_response/1
end

scope "/protected_resource", MyAppWeb.Protected do
    pipe_through [:basic_auth]
    ...
end
...

We had to define a callback function that will check that the credentials provided are valid, and optionally custom_response function where we can customize the error message:

# Authentication
defmodule MyAppWeb.Protected.Authentication do
  import Plug.Conn

  def authenticate(conn, login, password) do
    case check_login_and_password(login, passwod)
      true ->
        conn
        |> assign(:user_id, user.id)

      false ->
        halt(conn)
    end
  end

  def check_login_and_password(login, passwod)
    # check
  end
end

# Custom response
def unauthorized_response(conn) do
  conn
  |> Plug.Conn.put_resp_content_type("application/json")
  |> Plug.Conn.send_resp(401, ~s[{"message": "Unauthorized"}])
end

Once we have this ready, we can test it. The README of the BasicAuth plug gives us a hint on a using_basic_auth helper:

# example test
defmodule MyAppWeb.Protected.SampleControllerTest do
  use MyWeb.ConnCase

  @username Application.get_env(:myapp, :basic_auth)[:username]
  @password Application.get_env(:myapp, :basic_auth)[:password]

  describe "GET /protected_resource" do
    test "shows protected resource when credentials are valid", %{} do
      response =
        build_conn()
        |> using_basic_auth(@username, @password)
        |> get(Routes.sample_path(conn, :index))

      assert response.status == 200
    end

    test "error when credentials are invalid", %{} do
      response =
        build_conn()
        |> using_basic_auth(@username, "wrongpasswd")
        |> get(Routes.sample_path(conn, :index))

      assert response.status == 401
    end
  end

  defp using_basic_auth(conn, username, password) do
    header_content = "Basic " <> Base.encode64("#{username}:#{password}")
    conn |> put_req_header("authorization", header_content)
  end
end

Apart from the authentication helper that we pipe through with our connection we are also using valid credentials by default that we put to the text.exs file:

# Successful login for tests
config :myapp, :basic_auth,
  username: "test",
  password: "12dd9c47538129c7b48916d5d928e444"

Of course it’s up to you to make sure that a user with such credentials exist during the test run.

If we want to provide a “log out” option for the users, we can do so by sending wrong credentials to a protected resource (but they will see a new login popup).

Finally, if you are authorizing users to manipulate data on the server, make sure to use CSRF protection, because as I stated in the beginning, the Authorization header is sent automatically by the browser.

Leave a comment

Your email address will not be published. Required fields are marked *