From 31a833d73b9bd9a6c67bdad133ba70ade35981d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Armin=20Ahmetovi=C4=87?= Date: Wed, 11 Dec 2024 15:14:45 +0100 Subject: [PATCH] Handle API authentication errors for the RM 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 | 30 +++++++++++- .../views/error_view.ex | 27 +++++++++++ .../auth/auth_test.exs | 46 +++++++++++++++++-- .../test/support/jwt_test_helper.ex | 10 ++++ 4 files changed, 107 insertions(+), 6 deletions(-) diff --git a/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/controllers/fallback_controller.ex b/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/controllers/fallback_controller.ex index dd01fbcba..2d209e5ef 100644 --- a/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/controllers/fallback_controller.ex +++ b/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/controllers/fallback_controller.ex @@ -130,11 +130,37 @@ defmodule Astarte.RealmManagement.APIWeb.FallbackController do end # This is called when no JWT token is present - def auth_error(conn, {:unauthenticated, _reason}, _opts) do + def auth_error(conn, {:unauthenticated, :unauthenticated}, _opts) do conn |> put_status(:unauthorized) |> put_view(Astarte.RealmManagement.APIWeb.ErrorView) - |> render(:"401") + |> render(:missing_token) + end + + # Invalid JWT token + def auth_error(conn, {:invalid_token, :invalid_token}, _opts) do + conn + |> put_status(:unauthorized) + |> put_view(Astarte.RealmManagement.APIWeb.ErrorView) + |> 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.RealmManagement.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.RealmManagement.APIWeb.ErrorView) + |> render(:authorization_path_not_matched, %{method: conn.method, path: conn.request_path}) end # In all other cases, we reply with 403 diff --git a/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/views/error_view.ex b/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/views/error_view.ex index d2bacb5bc..c3303d5af 100644 --- a/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/views/error_view.ex +++ b/apps/astarte_realm_management_api/lib/astarte_realm_management_api_web/views/error_view.ex @@ -39,6 +39,33 @@ defmodule Astarte.RealmManagement.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. Ensure the realm name and endpoint are correctly specified in the request" + } + } + 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("interface_not_found.json", _assigns) do %{errors: %{detail: "Interface not found"}} end diff --git a/apps/astarte_realm_management_api/test/astarte_realm_management_api_web/auth/auth_test.exs b/apps/astarte_realm_management_api/test/astarte_realm_management_api_web/auth/auth_test.exs index 46dec3e04..c24b7e38b 100644 --- a/apps/astarte_realm_management_api/test/astarte_realm_management_api_web/auth/auth_test.exs +++ b/apps/astarte_realm_management_api/test/astarte_realm_management_api_web/auth/auth_test.exs @@ -41,7 +41,7 @@ defmodule Astarte.RealmManagement.APIWeb.AuthTest do describe "JWT" do test "no token returns 401", %{conn: conn} do conn = get(conn, @request_path) - assert json_response(conn, 401)["errors"]["detail"] == "Unauthorized" + assert json_response(conn, 401)["errors"]["detail"] == "Missing authorization token" end test "all access token returns the data", %{conn: conn} do @@ -89,7 +89,8 @@ defmodule Astarte.RealmManagement.APIWeb.AuthTest do ) |> get("#{@request_path}/suffix") - 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 "token for another path returns 403", %{conn: conn} do @@ -101,7 +102,8 @@ defmodule Astarte.RealmManagement.APIWeb.AuthTest do ) |> get(@request_path) - 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 "token for both paths returns the data", %{conn: conn} do @@ -125,7 +127,8 @@ defmodule Astarte.RealmManagement.APIWeb.AuthTest do ) |> get(@request_path) - 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 "token for both methods returns the data", %{conn: conn} do @@ -151,5 +154,40 @@ defmodule Astarte.RealmManagement.APIWeb.AuthTest do assert json_response(conn, 200)["data"] == @expected_data end + + test "invalid JWT token returns 401", %{conn: conn} do + conn = + put_req_header( + conn, + "authorization", + "bearer invalid_token" + ) + |> get(@request_path) + + 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(["^GET$::#{@valid_auth_path}"]) + + conn = + put_req_header(conn, "authorization", "bearer #{token}") + |> get(@request_path) + + assert json_response(conn, 401)["errors"]["detail"] == "Invalid JWT token" + end + + test "token with invalid path claim returns 403", %{conn: conn} do + conn = + put_req_header( + conn, + "authorization", + "bearer #{JWTTestHelper.gen_jwt_token(["^POST$::#{@non_matching_auth_path}"])}" + ) + |> get(@request_path) + + assert json_response(conn, 403)["errors"]["detail"] == + "Unauthorized access to #{conn.assigns.method} #{conn.assigns.path}. Please verify your permissions" + end end end diff --git a/apps/astarte_realm_management_api/test/support/jwt_test_helper.ex b/apps/astarte_realm_management_api/test/support/jwt_test_helper.ex index 577f79129..10b964c32 100644 --- a/apps/astarte_realm_management_api/test/support/jwt_test_helper.ex +++ b/apps/astarte_realm_management_api/test/support/jwt_test_helper.ex @@ -40,6 +40,16 @@ defmodule Astarte.RealmManagement.API.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