From f1af38606a47d7720dc3c4269f0596b1707911b6 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Sun, 22 Dec 2024 18:32:04 +0000 Subject: [PATCH] Protect API behind a bearer token --- config/config.exs | 2 +- config/runtime.exs | 8 +++++++- lib/music_library_web/auth.ex | 17 +++++++++++++++++ lib/music_library_web/router.ex | 3 ++- priv/gettext/default.pot | 2 +- .../controllers/collection_controller_test.exs | 16 +++++++++++++++- 6 files changed, 43 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 67962127..c530b62a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -11,7 +11,7 @@ config :music_library, ecto_repos: [MusicLibrary.Repo], generators: [timestamp_type: :utc_datetime, binary_id: true] -config :music_library, MusicLibraryWeb, login_password: "change me" +config :music_library, MusicLibraryWeb, login_password: "change me", api_token: "change me" # Configures the endpoint config :music_library, MusicLibraryWeb.Endpoint, diff --git a/config/runtime.exs b/config/runtime.exs index 203f4c87..6d97bb4a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -69,10 +69,16 @@ if config_env() == :prod do environment variable LOGIN_PASSWORD is missing. """ + api_token = + System.get_env("API_TOKEN") || + raise """ + environment variable API_TOKEN is missing. + """ + host = System.get_env("PHX_HOST") || "example.com" port = String.to_integer(System.get_env("PORT") || "4000") - config :music_library, MusicLibraryWeb, login_password: login_password + config :music_library, MusicLibraryWeb, login_password: login_password, api_token: api_token config :music_library, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") diff --git a/lib/music_library_web/auth.ex b/lib/music_library_web/auth.ex index 53b05099..0779cb02 100644 --- a/lib/music_library_web/auth.ex +++ b/lib/music_library_web/auth.ex @@ -7,11 +7,28 @@ defmodule MusicLibraryWeb.Auth do Plug.Crypto.secure_compare(login_password(), password) end + def require_api_token(conn, _opts) do + with ["Bearer " <> token] <- get_req_header(conn, "authorization"), + true <- Plug.Crypto.secure_compare(api_token(), token) do + conn + else + _ -> + conn + |> send_resp(:unauthorized, "Unauthorized API access") + |> halt() + end + end + defp login_password do Application.get_env(:music_library, MusicLibraryWeb) |> Keyword.fetch!(:login_password) end + defp api_token do + Application.get_env(:music_library, MusicLibraryWeb) + |> Keyword.fetch!(:api_token) + end + def require_logged_in(conn, _opts) do if get_session(conn, :logged_in) do conn diff --git a/lib/music_library_web/router.ex b/lib/music_library_web/router.ex index b61b967c..1936ad77 100644 --- a/lib/music_library_web/router.ex +++ b/lib/music_library_web/router.ex @@ -1,7 +1,7 @@ defmodule MusicLibraryWeb.Router do use MusicLibraryWeb, :router - import MusicLibraryWeb.Auth, only: [require_logged_in: 2] + import MusicLibraryWeb.Auth, only: [require_logged_in: 2, require_api_token: 2] pipeline :browser do plug :accepts, ["html"] @@ -14,6 +14,7 @@ defmodule MusicLibraryWeb.Router do pipeline :api do plug :accepts, ["json"] + plug :require_api_token end pipeline :logged_in do diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 14c92f39..944a86a0 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -335,7 +335,7 @@ msgstr "" msgid "Wishlist" msgstr "" -#: lib/music_library_web/auth.ex:20 +#: lib/music_library_web/auth.ex:37 #, elixir-autogen, elixir-format msgid "You must be logged in to access this page" msgstr "" diff --git a/test/music_library_web/controllers/collection_controller_test.exs b/test/music_library_web/controllers/collection_controller_test.exs index be37a85a..49697280 100644 --- a/test/music_library_web/controllers/collection_controller_test.exs +++ b/test/music_library_web/controllers/collection_controller_test.exs @@ -7,12 +7,26 @@ defmodule MusicLibraryWeb.CollectionControllerTest do %{record: record_fixture_with_artist("Steven Wilson")} end + defp api_token do + Application.get_env(:music_library, MusicLibraryWeb) + |> Keyword.fetch!(:api_token) + end + describe "GET /api/collection/latest" do setup [:create_record] - test "it returns the latest record", %{conn: conn, record: record} do + test "it requires authentication", %{conn: conn} do conn = get(conn, ~p"/api/collection/latest") + assert conn.status == 401 + end + + test "it returns the latest record", %{conn: conn, record: record} do + conn = + conn + |> put_req_header("authorization", "Bearer #{api_token()}") + |> get(~p"/api/collection/latest") + assert json_response(conn, 200) == %{ "artists" => ["Steven Wilson"], "title" => record.title,