171 lines
5.7 KiB
Elixir
171 lines
5.7 KiB
Elixir
defmodule Plug.BasicAuth do
|
|
@moduledoc """
|
|
Functionality for providing Basic HTTP authentication.
|
|
|
|
It is recommended to only use this module in production
|
|
if SSL is enabled and enforced. See `Plug.SSL` for more
|
|
information.
|
|
|
|
## Compile-time usage
|
|
|
|
If you have a single username and password, you can use
|
|
the `basic_auth/2` plug:
|
|
|
|
import Plug.BasicAuth
|
|
plug :basic_auth, username: "hello", password: "secret"
|
|
|
|
Or if you would rather put those in a config file:
|
|
|
|
# lib/your_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"
|
|
|
|
Once the user first accesses the page, the request will be denied
|
|
with reason 401 and the request is halted. The browser will then
|
|
prompt the user for username and password. If they match, then the
|
|
request succeeds.
|
|
|
|
Both approaches shown above rely on static configuration. Let's see
|
|
alternatives.
|
|
|
|
## Runtime-time usage
|
|
|
|
As any other Plug, we can use the `basic_auth` at runtime by simply
|
|
wrapping it in a function:
|
|
|
|
plug :auth
|
|
|
|
defp auth(conn, _opts) do
|
|
username = System.fetch_env!("AUTH_USERNAME")
|
|
password = System.fetch_env!("AUTH_PASSWORD")
|
|
Plug.BasicAuth.basic_auth(conn, username: username, password: password)
|
|
end
|
|
|
|
This approach is useful when both username and password are specified
|
|
upfront and available at runtime. However, you may also want to compute
|
|
a different password for each different user. In those cases, we can use
|
|
the low-level API.
|
|
|
|
## Low-level usage
|
|
|
|
If you want to provide your own authentication logic on top of Basic HTTP
|
|
auth, you can use the low-level functions. As an example, we define `:auth`
|
|
plug that extracts username and password from the request headers, compares
|
|
them against the database, and either assigns a `:current_user` on success
|
|
or responds with an error on failure.
|
|
|
|
plug :auth
|
|
|
|
defp auth(conn, _opts) do
|
|
with {user, pass} <- Plug.BasicAuth.parse_basic_auth(conn),
|
|
%User{} = user <- MyApp.Accounts.find_by_username_and_password(user, pass) do
|
|
assign(conn, :current_user, user)
|
|
else
|
|
_ -> conn |> Plug.BasicAuth.request_basic_auth() |> halt()
|
|
end
|
|
end
|
|
|
|
Keep in mind that:
|
|
|
|
* The supplied `user` and `pass` may be empty strings;
|
|
|
|
* If you are comparing the username and password with existing strings,
|
|
do not use `==/2` or pattern matching. Use `Plug.Crypto.secure_compare/2`
|
|
instead.
|
|
|
|
"""
|
|
import Plug.Conn
|
|
|
|
@doc """
|
|
Higher level usage of Basic HTTP auth.
|
|
|
|
See the module docs for examples.
|
|
|
|
## Options
|
|
|
|
* `:username` - the expected username
|
|
* `:password` - the expected password
|
|
* `:realm` - the authentication realm. The value is not fully
|
|
sanitized, so do not accept user input as the realm and use
|
|
strings with only alphanumeric characters and space
|
|
|
|
"""
|
|
@spec basic_auth(Plug.Conn.t(), [auth_option]) :: Plug.Conn.t()
|
|
when auth_option: {:username, String.t()} | {:password, String.t()} | {:realm, String.t()}
|
|
def basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
|
username = Keyword.fetch!(options, :username)
|
|
password = Keyword.fetch!(options, :password)
|
|
|
|
with {request_username, request_password} <- parse_basic_auth(conn),
|
|
valid_username? = Plug.Crypto.secure_compare(username, request_username),
|
|
valid_password? = Plug.Crypto.secure_compare(password, request_password),
|
|
true <- valid_username? and valid_password? do
|
|
conn
|
|
else
|
|
_ -> conn |> request_basic_auth(options) |> halt()
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Parses the request username and password from Basic HTTP auth.
|
|
|
|
It returns either `{user, pass}` or `:error`. Note the username
|
|
and password may be empty strings. When comparing the username
|
|
and password with the expected values, be sure to use
|
|
`Plug.Crypto.secure_compare/2`.
|
|
|
|
See the module docs for examples.
|
|
"""
|
|
@spec parse_basic_auth(Plug.Conn.t()) :: {user :: String.t(), password :: String.t()} | :error
|
|
def parse_basic_auth(%Plug.Conn{} = conn) do
|
|
with ["Basic " <> encoded_user_and_pass] <- get_req_header(conn, "authorization"),
|
|
{:ok, decoded_user_and_pass} <- Base.decode64(encoded_user_and_pass),
|
|
[user, pass] <- :binary.split(decoded_user_and_pass, ":") do
|
|
{user, pass}
|
|
else
|
|
_ -> :error
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Encodes a basic authentication header.
|
|
|
|
This can be used during tests:
|
|
|
|
put_req_header(conn, "authorization", encode_basic_auth("hello", "world"))
|
|
|
|
"""
|
|
@spec encode_basic_auth(String.t(), String.t()) :: String.t()
|
|
def encode_basic_auth(user, pass) when is_binary(user) and is_binary(pass) do
|
|
"Basic " <> Base.encode64("#{user}:#{pass}")
|
|
end
|
|
|
|
@doc """
|
|
Requests basic authentication from the client.
|
|
|
|
It sets the response to status 401 with "Unauthorized" as body.
|
|
The response is not sent though (nor the connection is halted),
|
|
allowing developers to further customize it.
|
|
|
|
## Options
|
|
|
|
* `:realm` - the authentication realm. The value is not fully
|
|
sanitized, so do not accept user input as the realm and use
|
|
strings with only alphanumeric characters and space
|
|
|
|
"""
|
|
@spec request_basic_auth(Plug.Conn.t(), [option]) :: Plug.Conn.t()
|
|
when option: {:realm, String.t()}
|
|
def request_basic_auth(%Plug.Conn{} = conn, options \\ []) when is_list(options) do
|
|
realm = Keyword.get(options, :realm, "Application")
|
|
escaped_realm = String.replace(realm, "\"", "")
|
|
|
|
conn
|
|
|> put_resp_header("www-authenticate", "Basic realm=\"#{escaped_realm}\"")
|
|
|> resp(401, "Unauthorized")
|
|
end
|
|
end
|