Plug.BasicAuth
is now part of Plug.
This is an Elixir Plug for adding basic authentication into an application.
The plug can be configured to use:
- Static username and password in application configuration
-OR-
- Your own custom authentication function
Note that if using option (1), prior to 2.2.1 the library was vulnerable to timing attacks;
we suggest updating ~> 2.2.2
.
If you are using your own custom authentication function, then you are are on your own. (Plug.Crypto.secure_compare/2 is something that may help you compare binaries in constant time.)
Add the package as a dependency in your Elixir project using something along the lines of:
defp deps do
[{:basic_auth, "~> 2.2.2"}]
end
Add into the top of a controller, or into a router pipeline a plug declaration like:
plug BasicAuth, use_config: {:your_app, :your_config}
Where :your_app and :your_config should refer to values in your application config.
In your configuration you can set values directly, eg
config :your_app, your_config: [
username: "admin",
password: "simple_password",
realm: "Admin Area"
]
All configuration is read at runtime to support using REPLACE_OS_VARS as part of a release.
or choose to get one (or all) from environment variables, eg
config :basic_auth, my_auth_with_system: [
username: {:system, "BASIC_AUTH_USERNAME"},
password: {:system, "BASIC_AUTH_PASSWORD"},
realm: {:system, "BASIC_AUTH_REALM"}
]
Alternatively you can provide a custom function to the plug to authenticate the user any way you want, such as finding the user from a database.
plug BasicAuth, callback: &User.find_by_username_and_password/3
(or optionally provide a realm)
plug BasicAuth, callback: &User.find_by_username_and_password/3, realm: "Area 51"
Where :callback is your custom authentication function that takes a conn, username
and a password and returns a conn. Your function must return Plug.Conn.halt(conn)
if authentication fails, otherwise you can use Plug.Conn.assign(conn, :current_user, ...)
to enhance the conn with variables or session for your controller.
The function must have an arity of 3, and be of the form
@spec myfunction(Plug.Conn.t, String.t, String.t) :: Plug.Conn.t
It will receive a connection, username, and password.
Easy as that!
If you want a custom unauthorised response body you can configure a custom_response
:
plug BasicAuth,
use_config: {:your_app, :your_config},
custom_response: &YouApp.Helpers.unauthorized_response/1
or
plug BasicAuth,
callback: &User.find_by_username_and_password/3,
realm: "Area 51",
custom_response: &YouApp.Helpers.unauthorized_response/1
Where :custom_response
is a custom response function that takes a conn
. For example:
def unauthorized_response(conn) do
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(401, ~s[{"message": "Unauthorized"}])
end
If you're looking to authenticate only for a subset of actions in a controller you can use plug's when action in
syntax as shown below
plug BasicAuth, [use_config: {: your_app, : your_config}] when action in [:edit, :delete]
additionally you can exclude specific actions using not
plug BasicAuth, [use_config: {: your_app, : your_config}] when not action in [:index, :show]
If you're storing credentials within configuration files, we can reuse them within our test files
directly using snippets like Application.get_env(:basic_auth)[:username]
.
Any controller that makes use of basic authentication, will need an additional header injected into the connection in order for your tests to continue to work. The following is a brief snippet of how to get started. There is a more detailed blog post that explains a bit more about what needs to be done.
At the top of my controller test I have something that looks like:
@username Application.get_env(:the_app, :basic_auth)[:username]
@password Application.get_env(:the_app, :basic_auth)[:password]
defp using_basic_auth(conn, username, password) do
header_content = "Basic " <> Base.encode64("#{username}:#{password}")
conn |> put_req_header("authorization", header_content)
end
Then for any tests, I can simply pipe in this helper method to the connection process:
test "GET / successfully renders when basic auth credentials supplied", %{conn: conn} do
conn = conn
|> using_basic_auth(@username, @password)
|> get("/admin/users")
assert html_response(conn, 200) =~ "Users"
end
And a test case without basic auth for completeness:
test "GET / without basic auth credentials prevents access", %{conn: conn} do
conn = conn
|> get("/admin/users")
assert response(conn, 401) =~ "401 Unauthorized"
end