# Copyright(c) 2015-2023 ACCESS CO., LTD. All rights reserved. use Croma defmodule Antikythera.Plug.BasicAuthentication do @moduledoc """ Plug to restrict access to controller action by basic authentication. ## Usage ### Static username/password: using gear config The following line installs a plug that checks username/password in incoming requests against `"BASIC_AUTHENTICATION_ID"` and `"BASIC_AUTHENTICATION_PW"` in your gear's gear config. plug Antikythera.Plug.BasicAuthentication, :check_with_config, [] ### Dynamic username/password: using module-function pair. In the following case you can check the username/password pair with arbitrary logic by the specified function. plug Antikythera.Plug.BasicAuthentication, :check_with_fun, [mod: YourGear.AuthModule, fun: :function_name] The given function (`YourGear.AuthModule.function_name/3` in this case) must have the following type signature: - receives (1) a `Antikythera.Conn.t`, (2) username, and (3) password - returns (1) `{:ok, Antikythera.Conn.t}` if successfully authenticated, (2) `:error` otherwise """ alias Antikythera.{Conn, Crypto} alias AntikytheraCore.Ets.ConfigCache alias AntikytheraCore.Config.Gear, as: GearConfig defun check_with_config(conn :: v[Conn.t()], _opts :: any) :: Conn.t() do %GearConfig{kv: %{"BASIC_AUTHENTICATION_ID" => id, "BASIC_AUTHENTICATION_PW" => pw}} = ConfigCache.Gear.read(conn.context.gear_name) check_impl(conn, fn input_id, input_pw -> if Crypto.secure_compare(input_id, id) and Crypto.secure_compare(input_pw, pw) do {:ok, conn} else :error end end) end defun check_with_fun(conn :: v[Conn.t()], opts :: [{:mod | :fun, atom}]) :: Conn.t() do m = Keyword.fetch!(opts, :mod) f = Keyword.fetch!(opts, :fun) check_impl(conn, fn id, pw -> apply(m, f, [conn, id, pw]) end) end defp check_impl(conn1, f) do with {id, pw} <- Conn.get_req_header(conn1, "authorization") |> decode_auth_header(), {:ok, conn2} <- f.(id, pw) do conn2 else _ -> halt_with_401(conn1) end end defp decode_auth_header("Basic " <> encoded_cred) do case Base.decode64(encoded_cred) do {:ok, name_and_pass} -> case :binary.split(name_and_pass, ":") do [n, p] -> {n, p} _ -> nil end :error -> nil end end defp decode_auth_header(_), do: nil defp halt_with_401(conn) do gear_name = conn.context.gear_name Conn.put_resp_header(conn, "www-authenticate", "Basic realm=\"#{gear_name}\"") |> Conn.put_status(401) |> Conn.put_resp_body("Access denied.") end end