From a29db4c23a1b1a8b0248930ecce7e49e1d869052 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armin=20Ahmetovi=C4=87?= Date: Mon, 13 Jan 2025 10:10:47 +0100 Subject: [PATCH] Handle API authentication errors for the Pairing API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expand the Fallback controller with some failed auth functions - Add additional error views with useful feedback messages - Add the gen_jwt_token_with_wrong_signature function in the JWTTestHelper - Add the necessary auth tests and adapt existing ones Signed-off-by: Armin Ahmetović --- .../controllers/fallback_controller.ex | 34 ++++++++++++++++--- .../views/error_view.ex | 26 ++++++++++++++ .../auth/auth_test.exs | 33 ++++++++++++++++-- .../test/support/jwt_test_helper.ex | 10 ++++++ 4 files changed, 96 insertions(+), 7 deletions(-) diff --git a/apps/astarte_pairing_api/lib/astarte_pairing_api_web/controllers/fallback_controller.ex b/apps/astarte_pairing_api/lib/astarte_pairing_api_web/controllers/fallback_controller.ex index a669bfa2a..6a18d1f3a 100644 --- a/apps/astarte_pairing_api/lib/astarte_pairing_api_web/controllers/fallback_controller.ex +++ b/apps/astarte_pairing_api/lib/astarte_pairing_api_web/controllers/fallback_controller.ex @@ -61,17 +61,41 @@ defmodule Astarte.Pairing.APIWeb.FallbackController do |> render(:"403") end - # This is the final call made by EnsureAuthenticated - def auth_error(conn, {:unauthenticated, reason}, _opts) do - _ = - Logger.info("Refusing unauthenticated request: #{inspect(reason)}.", tag: "unauthenticated") + # This is called when no JWT token is present + def auth_error(conn, {:unauthenticated, :unauthenticated}, _opts) do + conn + |> put_status(:unauthorized) + |> put_view(Astarte.Pairing.APIWeb.ErrorView) + |> render(:missing_token) + end + # Invalid JWT token + def auth_error(conn, {:invalid_token, :invalid_token}, _opts) do conn |> put_status(:unauthorized) |> put_view(Astarte.Pairing.APIWeb.ErrorView) - |> render(:"401") + |> render(:invalid_token) + end + + # TODO: add another auth_error for invalid_token_signature + + # Invalid authorized path + def call(conn, {:error, :invalid_auth_path}) do + conn + |> put_status(:unauthorized) + |> put_view(Astarte.Pairing.APIWeb.ErrorView) + |> render(:invalid_auth_path) + end + + # Path not authorized + def auth_error(conn, {:unauthorized, :authorization_path_not_matched}, _opts) do + conn + |> put_status(:forbidden) + |> put_view(Astarte.Pairing.APIWeb.ErrorView) + |> render(:authorization_path_not_matched, %{method: conn.method, path: conn.request_path}) end + # In all other cases, we reply with 403 def auth_error(conn, _reason, _opts) do conn |> put_status(:forbidden) diff --git a/apps/astarte_pairing_api/lib/astarte_pairing_api_web/views/error_view.ex b/apps/astarte_pairing_api/lib/astarte_pairing_api_web/views/error_view.ex index 35067c476..7e79ec5e7 100644 --- a/apps/astarte_pairing_api/lib/astarte_pairing_api_web/views/error_view.ex +++ b/apps/astarte_pairing_api/lib/astarte_pairing_api_web/views/error_view.ex @@ -31,6 +31,32 @@ defmodule Astarte.Pairing.APIWeb.ErrorView do %{errors: %{detail: "Forbidden"}} end + def render("missing_token.json", _assigns) do + %{errors: %{detail: "Missing authorization token"}} + end + + def render("invalid_token.json", _assigns) do + %{errors: %{detail: "Invalid JWT token"}} + end + + # TODO: add error message for invalid_token_signature + + def render("invalid_auth_path.json", _assigns) do + %{ + errors: %{ + detail: "Authorization failed due to an invalid path" + } + } + end + + def render("authorization_path_not_matched.json", %{method: method, path: path}) do + %{ + errors: %{ + detail: "Unauthorized access to #{method} #{path}. Please verify your permissions" + } + } + end + def render("404.json", _assigns) do %{errors: %{detail: "Page not found"}} end diff --git a/apps/astarte_pairing_api/test/astarte_pairing_api_web/auth/auth_test.exs b/apps/astarte_pairing_api/test/astarte_pairing_api_web/auth/auth_test.exs index c30c9a401..a01b38aff 100644 --- a/apps/astarte_pairing_api/test/astarte_pairing_api_web/auth/auth_test.exs +++ b/apps/astarte_pairing_api/test/astarte_pairing_api_web/auth/auth_test.exs @@ -45,6 +45,11 @@ defmodule Astarte.Pairing.APIWeb.AuthTest do {:ok, conn: conn} end + test "no token returns 401", %{conn: conn} do + conn = post(conn, agent_path(conn, :create, @realm), data: @create_attrs) + assert json_response(conn, 401)["errors"]["detail"] == "Missing authorization token" + end + test "succeeds with specific authorizations", %{conn: conn} do register_authorizations = ["POST::agent/devices"] @@ -86,7 +91,8 @@ defmodule Astarte.Pairing.APIWeb.AuthTest do |> authorize_conn(register_authorizations) |> post(agent_path(conn, :create, @realm), data: @create_attrs) - assert json_response(conn, 403)["errors"]["detail"] == "Forbidden" + assert json_response(conn, 403)["errors"]["detail"] == + "Unauthorized access to #{conn.assigns.method} #{conn.assigns.path}. Please verify your permissions" end test "fails with authorization for different method", %{conn: conn} do @@ -97,7 +103,30 @@ defmodule Astarte.Pairing.APIWeb.AuthTest do |> authorize_conn(register_authorizations) |> post(agent_path(conn, :create, @realm), data: @create_attrs) - assert json_response(conn, 403)["errors"]["detail"] == "Forbidden" + assert json_response(conn, 403)["errors"]["detail"] == + "Unauthorized access to #{conn.assigns.method} #{conn.assigns.path}. Please verify your permissions" + end + + test "invalid JWT token returns 401", %{conn: conn} do + conn = + put_req_header( + conn, + "authorization", + "bearer invalid_token" + ) + |> post(agent_path(conn, :create, @realm), data: @create_attrs) + + assert json_response(conn, 401)["errors"]["detail"] == "Invalid JWT token" + end + + test "token with mismatched signature returns 401", %{conn: conn} do + token = JWTTestHelper.gen_jwt_token_with_wrong_signature(["^POST$::agent/devices"]) + + conn = + put_req_header(conn, "authorization", "bearer #{token}") + |> post(agent_path(conn, :create, @realm), data: @create_attrs) + + assert json_response(conn, 401)["errors"]["detail"] == "Invalid JWT token" end end diff --git a/apps/astarte_pairing_api/test/support/jwt_test_helper.ex b/apps/astarte_pairing_api/test/support/jwt_test_helper.ex index fb1e25ce3..1e46d145c 100644 --- a/apps/astarte_pairing_api/test/support/jwt_test_helper.ex +++ b/apps/astarte_pairing_api/test/support/jwt_test_helper.ex @@ -40,6 +40,16 @@ defmodule Astarte.Pairing.APIWeb.JWTTestHelper do jwt end + def gen_jwt_token_with_wrong_signature(authorization_paths) do + valid_token = gen_jwt_token(authorization_paths) + + [header, payload, _signature] = String.split(valid_token, ".") + + fake_signature = "fake_signature" + + "#{header}.#{payload}.#{fake_signature}" + end + def gen_jwt_all_access_token do gen_jwt_token([".*::.*"]) end