Phoenix Plug.BasicAuth config at runtime

HTTP Basic Auth a simple way of restricting access to an app, without introducing a full-blown user authentication system.

Luckily Plug comes with Plug.BasicAuth already built-in for us to use.

This is the usage example from the docs :

# lib/my_app.ex
import Plug.BasicAuth
plug :basic_auth, Application.compile_env(:my_app, :basic_auth)

# config/config.exs
config :my_app, :basic_auth, username: "hello", password: "secret"

Seems simple enough! In a real-life Phoenix app, we must probably:

So something like:

# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  # ...
  import Plug.BasicAuth, only: [basic_auth: 2]
  
  plug :basic_auth, Application.compile_env!(:my_app, :basic_auth)
  # ...
end

# config/config.exs
config :my_app, :basic_auth,
  username: System.get_env("AUTH_USERNAME", "user")
  password: System.get_env("AUTH_PASSWORD", "password")

The problem

But what happens if we do it like this and load the config in config.exs, as suggested?

This might not seem to matter much while developing locally, but, as soon as we want to build and deploy our app, it means that:

These do sound like a bit of a pain...
👉 In fact, when it comes to env variables and external config, the elixir docs actively discourage using config.exs, in favour of runtime.exs, which allows us to read configuration during (surprise) runtime.

Let's fix this

So, can we just move our config into runtime.exs and be done?

Not right away! Our plug declaration is currently reading configuration at compile time. How do we know this? If you remember our Endpoint definition from above:

defmodule MyAppWeb.Endpoint do
  # ...
  plug :basic_auth, Application.compile_env!(:my_app, :basic_auth)
  # ...
end
  1. We are using Application.compile_env!/2. This reads the application environment at compile time
  2. And we are calling it in the module body. This is evaluated at compile time.

To fix this, we need to find alternatives for the two things above, so we can read the application environment at runtime:

  1. Use a runtime-alternative to Application.compile_env!/2. E.g. Application.fetch_env!/2
  2. Call within a function body, where it will be evaluated at runtime.

So how do we do this? One way to do this would be to make our own wrapper Plug around the built-in one.

We have two options here: as a function, or as a module.
Picking the simplest of the two, let's go with a function:

# lib/my_app_web/endpoint.ex
defmodule MyAppWeb.Endpoint do
  # ...
  plug :basic_auth

  # ...
  def basic_auth(conn, _opts) do
    basic_auth_config = Application.fetch_env!(:my_app, :basic_auth)
    Plug.BasicAuth.basic_auth(conn, basic_auth_config)
  end
end

We are now reading our config at runtime :)
Then we can just move our config to runtime.exs:

# config/runtime.exs
config :my_app, :basic_auth,
  username: System.get_env("AUTH_USERNAME", "user")
  password: System.get_env("AUTH_PASSWORD", "password")

That's it ✨

PS: If we then want to deploy our app to something like fly.io, we can then configure our secrets like any other secret:

fly secrets set AUTH_USERNAME=some-other-user AUTH_PASSWORD=very-secret-password

or use whatever equivalent env variable/secret management system.

Key takeaways