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). Secondly that encoding password using Base64 is not a security feature and you need HTTPS.
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
UPDATE: The above way got deprecated and now issues the following warning:
warning: BasicAuth.init/1 is deprecated
We have to rewrite it as our function plug taking advantage of the BasicAuth
module functions directly:
# in router
pipeline :basic_auth do
plug :my_basic_auth
end
defp my_basic_auth(conn, _opts) do
{user, pass} = Plug.BasicAuth.parse_basic_auth(conn)
case MyAppWeb.API.Authentication.authenticate(conn, user, pass) do
{:ok, conn} ->
conn
{:error, conn} ->
conn |> MyAppWeb.API.Helpers.unauthorized_response()
end
end
# Authentication
defmodule MyAppWeb.Protected.Authentication do
import Plug.Conn
def authenticate(conn, login, password) do
case check_login_and_password(login, passwod)
true ->
conn = conn
|> assign(:user_id, user.id)
{:ok, conn}
false ->
{:error, halt(conn)}
end
end
def check_login_and_password(login, passwod)
# check
end
end
What we do is parsing the user and pass combination with parse_basic_auth/1
function and then returning the conn
in tuples to differentiate success from failure.
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.
Get Test Driving Rails while it's in prerelease.