Skip to content

Commit

Permalink
Handle API authentication errors for the Pairing API
Browse files Browse the repository at this point in the history
- 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ć <[email protected]>
  • Loading branch information
arahmarchak committed Jan 14, 2025
1 parent 76466bb commit a7974b2
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,46 @@ 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")
# 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

# 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(:invalid_token)
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

def auth_error(conn, {:unauthenticated, _reason}, _opts) do
conn
|> put_status(:unauthorized)
|> put_view(Astarte.Pairing.APIWeb.ErrorView)
|> render(:"401")
end

# In all other cases, we reply with 403
def auth_error(conn, _reason, _opts) do
conn
|> put_status(:forbidden)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down
10 changes: 10 additions & 0 deletions apps/astarte_pairing_api/test/support/jwt_test_helper.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit a7974b2

Please sign in to comment.