diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e5cffbf47..d67b29a08 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -332,35 +332,35 @@ jobs: build-containers: name: Build containers - needs: - - static-code-analysis - - test - if: (github.event_name == 'push' && github.ref_name == 'main') || github.event_name == 'workflow_dispatch' + # needs: + # - static-code-analysis + # - test + # if: (github.event_name == 'push' && github.ref_name == 'main') || github.event_name == 'workflow_dispatch' uses: trento-project/web/.github/workflows/build-containers.yaml@main strategy: matrix: include: - MIX_ENV: prod - tag: "${{ (github.event_name == 'push' && github.ref_name == 'main' && 'rolling') || github.sha }}" + tag: tesing-introspection - MIX_ENV: demo - tag: demo + tag: tesing-introspection with: image_name: trento-wanda MIX_ENV: ${{ matrix.MIX_ENV }} tag: ${{ matrix.tag }} - deploy-demo: - runs-on: ubuntu-24.04 - if: vars.DEPLOY_DEMO == 'true' - needs: - - build-containers - steps: - - name: Remotely trigger trento-web demo deployment - uses: peter-evans/repository-dispatch@v3 - with: - token: ${{ secrets.WEB_REPO_DISPATCH_PAT }} - repository: ${{ github.repository_owner }}/${{ vars.DEMO_TRIGGER_TARGET || 'web' }} - event-type: deploy-demo + # deploy-demo: + # runs-on: ubuntu-24.04 + # if: vars.DEPLOY_DEMO == 'true' + # needs: + # - build-containers + # steps: + # - name: Remotely trigger trento-web demo deployment + # uses: peter-evans/repository-dispatch@v3 + # with: + # token: ${{ secrets.WEB_REPO_DISPATCH_PAT }} + # repository: ${{ github.repository_owner }}/${{ vars.DEMO_TRIGGER_TARGET || 'web' }} + # event-type: deploy-demo generate-docs: name: Generate project documentation diff --git a/config/config.exs b/config/config.exs index 4511440c2..6cd85af3e 100644 --- a/config/config.exs +++ b/config/config.exs @@ -87,7 +87,7 @@ config :rustler_precompiled, :force_build, rhai_rustler: true config :wanda, cors_enabled: true, - jwt_authentication_enabled: true, + token_authentication_enabled: true, operations_enabled: true, date_service: Wanda.Support.DateService, oas_server_url: nil @@ -95,6 +95,8 @@ config :wanda, config :bodyguard, default_error: :forbidden +config :wanda, :auth_server, url: "http://localhost:4000" + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/runtime.exs b/config/runtime.exs index db87eff72..82cd391b6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -106,12 +106,12 @@ if config_env() in [:prod, :demo] do origin: [cors_origin] end - jwt_authentication_enabled = System.get_env("JWT_AUTHENTICATION_ENABLED", "true") == "true" + token_authentication_enabled = System.get_env("TOKEN_AUTHENTICATION_ENABLED", "true") == "true" config :wanda, - jwt_authentication_enabled: jwt_authentication_enabled + token_authentication_enabled: token_authentication_enabled - if jwt_authentication_enabled do + if token_authentication_enabled do config :joken, access_token_signer: System.get_env("ACCESS_TOKEN_ENC_SECRET") || @@ -133,6 +133,15 @@ if config_env() in [:prod, :demo] do ) ) ] + + auth_server_url = + System.get_env("AUTH_SERVER_URL") || + raise """ + environment variable AUTH_SERVER_URL is missing. + For example: http://localhost:4000 + """ + + config :wanda, :auth_server, url: auth_server_url end if config_env() === :demo do diff --git a/config/test.exs b/config/test.exs index 61f8b331f..b8671f10e 100644 --- a/config/test.exs +++ b/config/test.exs @@ -106,7 +106,9 @@ config :joken, access_token_signer: "s2ZdE+3+ke1USHEJ5O45KT364KiXPYaB9cJPdH3p60t8yT0nkLexLBNw8TFSzC7k" config :wanda, - jwt_authentication_enabled: false + token_authentication_enabled: false config :wanda, Wanda.Executions.FakeGatheredFacts, demo_facts_config: "test/fixtures/demo/fake_facts_test.yaml" + +config :exvcr, global_mock: true diff --git a/lib/wanda_web/auth/jwt_auth_plug.ex b/lib/wanda_web/auth/auth_plug.ex similarity index 57% rename from lib/wanda_web/auth/jwt_auth_plug.ex rename to lib/wanda_web/auth/auth_plug.ex index 96c3c91e8..c3f92ace5 100644 --- a/lib/wanda_web/auth/jwt_auth_plug.ex +++ b/lib/wanda_web/auth/auth_plug.ex @@ -1,6 +1,6 @@ -defmodule WandaWeb.Auth.JWTAuthPlug do +defmodule WandaWeb.Auth.AuthPlug do @moduledoc """ - Plug responsible for reading the JWT from the authorization header and + Plug responsible for reading the Token from the authorization header and validating it. If the token is valid, the user_id is added to the private section of the @@ -11,26 +11,26 @@ defmodule WandaWeb.Auth.JWTAuthPlug do import Plug.Conn - alias WandaWeb.Auth.AccessToken + alias WandaWeb.Auth.Client.AuthClient require Logger def init(opts), do: opts @doc """ - Read, validate and decode the JWT from authorization header at each call + Read, validate and decode the Token from authorization header at each call """ def call(conn, _) do authenticate(conn) end defp authenticate(conn) do - with {:ok, jwt_token} <- read_token(conn), - {:ok, %{"sub" => sub, "abilities" => abilities}} <- - AccessToken.verify_and_validate(jwt_token) do + with {:ok, token} <- read_token(conn), + {:ok, %{active: true, sub: sub, abilities: abilities}} <- + AuthClient.introspect_token(token) do conn |> put_private(:user_id, sub) - |> put_private(:abilities, abilities_to_atom_map(abilities)) + |> put_private(:abilities, abilities) else _ -> conn @@ -52,15 +52,4 @@ defmodule WandaWeb.Auth.JWTAuthPlug do {:error, :no_token} end end - - defp abilities_to_atom_map(abilities) when is_list(abilities) do - Enum.map(abilities, fn %{"name" => name, "resource" => resource} -> - %{ - name: name, - resource: resource - } - end) - end - - defp abilities_to_atom_map(_), do: [] end diff --git a/lib/wanda_web/auth/client/auth_client.ex b/lib/wanda_web/auth/client/auth_client.ex new file mode 100644 index 000000000..71ccd9678 --- /dev/null +++ b/lib/wanda_web/auth/client/auth_client.ex @@ -0,0 +1,19 @@ +defmodule WandaWeb.Auth.Client.AuthClient do + @moduledoc """ + Client for interacting with the authentication server. + """ + + @type introspected_token :: %{ + active: boolean(), + sub: integer(), + abilities: list(%{name: String.t(), resource: String.t()}) + } + + @callback introspect_token(token :: String.t()) :: + {:ok, introspected_token()} | {:error, atom()} + + def introspect_token(token), do: impl().introspect_token(token) + + defp impl, + do: Application.get_env(:wanda, :auth_client, WandaWeb.Auth.Client.HttpClient) +end diff --git a/lib/wanda_web/auth/client/auth_client_http.ex b/lib/wanda_web/auth/client/auth_client_http.ex new file mode 100644 index 000000000..648c54537 --- /dev/null +++ b/lib/wanda_web/auth/client/auth_client_http.ex @@ -0,0 +1,53 @@ +defmodule WandaWeb.Auth.Client.HttpClient do + @moduledoc """ + Http AuthClient implementation. + """ + + @behaviour WandaWeb.Auth.Client.AuthClient + + alias WandaWeb.Auth.Client.AuthClient + + require Logger + + @impl AuthClient + def introspect_token(token) do + with response <- make_introspect_request(token), + {:ok, body} <- get_response_body(response), + {:ok, _} = result <- decode_response(body) do + result + end + end + + defp make_introspect_request(token) do + HTTPoison.post( + "#{auth_server_url()}/api/session/token/introspect", + Jason.encode!(%{"token" => token}), + [{"Content-type", "application/json"}] + ) + end + + defp get_response_body({:ok, %HTTPoison.Response{status_code: 200, body: body}}), + do: {:ok, body} + + defp get_response_body(error) do + Logger.error("Unable to retrieve token introspection response body. Error: #{inspect(error)}") + + {:error, :unable_to_get_introspect_response} + end + + defp decode_response(body) do + case Jason.decode(body, keys: :atoms) do + {:ok, _} = result -> + result + + error -> + Logger.error("Unable to decode response body. Error: #{inspect(error)}") + + {:error, :cannot_decode_introspect_response} + end + end + + defp auth_server_url do + Application.fetch_env!(:wanda, :auth_server)[:url] + end +end diff --git a/lib/wanda_web/router.ex b/lib/wanda_web/router.ex index 1f9fdbba7..a34d6cf8e 100644 --- a/lib/wanda_web/router.ex +++ b/lib/wanda_web/router.ex @@ -42,8 +42,8 @@ defmodule WandaWeb.Router do pipeline :protected_api do plug Unplug, - if: {Unplug.Predicates.AppConfigEquals, {:wanda, :jwt_authentication_enabled, true}}, - do: WandaWeb.Auth.JWTAuthPlug + if: {Unplug.Predicates.AppConfigEquals, {:wanda, :token_authentication_enabled, true}}, + do: WandaWeb.Auth.AuthPlug end scope "/" do diff --git a/mix.exs b/mix.exs index 2696f19a0..3d3a2f4a7 100644 --- a/mix.exs +++ b/mix.exs @@ -19,7 +19,11 @@ defmodule Wanda.MixProject do test_coverage: [tool: ExCoveralls], preferred_cli_env: [ coveralls: :test, - "coveralls.github": :test + "coveralls.github": :test, + vcr: :test, + "vcr.delete": :test, + "vcr.check": :test, + "vcr.show": :test ], dialyzer: [plt_add_apps: [:ex_unit]] ] @@ -128,7 +132,9 @@ defmodule Wanda.MixProject do # required overrides to upgrade to elixir 1.15.7 and erlang otp 26 # https://stackoverflow.com/questions/76562092/hi-i-had-created-elixir-project-with-phoenix-framework-there-is-yaml-file-when {:ecto, "~> 3.10", override: true}, - {:bodyguard, "~> 2.4"} + {:bodyguard, "~> 2.4"}, + {:httpoison, "~> 2.0"}, + {:exvcr, "~> 0.11", only: :test} ] end diff --git a/mix.lock b/mix.lock index e33d9126a..0bce1d90a 100644 --- a/mix.lock +++ b/mix.lock @@ -4,6 +4,7 @@ "bodyguard": {:hex, :bodyguard, "2.4.3", "5faec1c7a346b3a6bac0b63aa3b2ae05b8dab6a5e4f8384e21577710498f8b56", [:mix], [{:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "5bb6bcc04871e18d97da5822a4d5d25ec38158447cb767b2eea3e2eb99cdc351"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -21,20 +22,30 @@ "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"}, + "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, + "exvcr": {:hex, :exvcr, "0.17.1", "3bae83d698a464a48212ad87c8ea4bcfb6bd76d53b937129472764e557616228", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 1.0", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "a39d86980da183011366a878972e0ed43a5441814b701edd2e11360564e7bcab"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, "gen_rmq": {:git, "https://github.com/cdimonaco/gen_rmq.git", "b8041734b4e0af61a851087652cb005e0bdec63e", [ref: "v5.0.1"]}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, + "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, + "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "meck": {:hex, :meck, "1.1.0", "e65a3d84d7b418afcb2c8efde2b5106a98eefb7fe8e5f7e324fe9765122aefa2", [:rebar3], [], "hexpm", "0fdfbd4d4f9eb9251ba621f4740a6e450e2a7d0e5e07a37830c5cf632f2ca8bb"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"}, + "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, @@ -51,12 +62,14 @@ "rhai_rustler": {:hex, :rhai_rustler, "1.2.0", "6825ab7814fa521fce7817087764eee86d24b3ce0fc9c8922f0d2ac81ce8edaf", [:mix], [{:rustler, "~> 0.36.0", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.8.0", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "062b66bbe64cf16f8cb85d5408faec6d94a7431c01a864999d1227a7d108f689"}, "rustler": {:hex, :rustler, "0.36.1", "2d4b1ff57ea2789a44756a40dbb5fbb73c6ee0a13d031dcba96d0a5542598a6a", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "f3fba4ad272970e0d1bc62972fc4a99809651e54a125c5242de9bad4574b2d02"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, "telemetry_poller": {:hex, :telemetry_poller, "1.2.0", "ba82e333215aed9dd2096f93bd1d13ae89d249f82760fcada0850ba33bac154b", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7216e21a6c326eb9aa44328028c34e9fd348fb53667ca837be59d0aa2a0156e8"}, "thoas": {:hex, :thoas, "1.2.1", "19a25f31177a17e74004d4840f66d791d4298c5738790fa2cc73731eb911f195", [:rebar3], [], "hexpm", "e38697edffd6e91bd12cea41b155115282630075c2a727e7a6b2947f5408b86a"}, "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, "trento_contracts": {:git, "https://github.com/trento-project/contracts.git", "67fe0387f4f61b5d53f081fec3caf63919d85bac", [sparse: "elixir", ref: "67fe0387f4f61b5d53f081fec3caf63919d85bac"]}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unplug": {:hex, :unplug, "1.1.0", "acd8c40dfda63a831b01534307c8cc44726389429294a5ed44d7f89fb01ccd02", [:mix], [{:plug, "~> 1.8", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "a3b302125ed60b658a9a7c0dff6941050bfc56dc77a0bca72facdb743159898f"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, diff --git a/test/test_helper.exs b/test/test_helper.exs index e18c2992b..cc051424f 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -16,6 +16,9 @@ Application.put_env(:joken, :current_time_adapter, Joken.CurrentTime.Mock) Mox.defmock(Wanda.Support.DateService.Mock, for: Wanda.Support.DateService) +Mox.defmock(WandaWeb.Auth.Client.AuthClientMock, for: WandaWeb.Auth.Client.AuthClient) +Application.put_env(:wanda, :auth_client, WandaWeb.Auth.Client.AuthClientMock) + ExUnit.start(capture_log: true) Faker.start() diff --git a/test/wanda_web/auth/auth_plug_test.exs b/test/wanda_web/auth/auth_plug_test.exs new file mode 100644 index 000000000..645b9f64b --- /dev/null +++ b/test/wanda_web/auth/auth_plug_test.exs @@ -0,0 +1,85 @@ +defmodule WandaWeb.Auth.AuthPlugTest do + use WandaWeb.ConnCase, async: true + + import Mox + import Wanda.Factory + + alias Faker.Random.Elixir, as: RandomElixir + + alias WandaWeb.Auth.AuthPlug + + setup [:set_mox_from_context, :verify_on_exit!] + + describe "call/2" do + test "should return the connection with the user related information" do + active_token = Faker.String.base64(32) + + user_id = RandomElixir.random_between(1, 100) + abilities = build_list(3, :ability) + + expect(WandaWeb.Auth.Client.AuthClientMock, :introspect_token, fn ^active_token -> + {:ok, + %{ + active: true, + sub: user_id, + abilities: abilities + }} + end) + + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> active_token) + |> AuthPlug.call([]) + + refute conn.halted + assert conn.private.user_id == user_id + + assert conn.private.abilities == abilities + end + + test "should return the connection with empty user abilities" do + active_token = Faker.String.base64(32) + + user_id = RandomElixir.random_between(1, 100) + + expect(WandaWeb.Auth.Client.AuthClientMock, :introspect_token, fn ^active_token -> + {:ok, + %{ + active: true, + sub: user_id, + abilities: [] + }} + end) + + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> active_token) + |> AuthPlug.call([]) + + assert conn.private.abilities == [] + end + + test "should be halted and have unauthorized status if an inactive token was passed" do + inactive_token = Faker.String.base64(32) + + expect(WandaWeb.Auth.Client.AuthClientMock, :introspect_token, fn ^inactive_token -> + {:ok, %{active: false}} + end) + + conn = + build_conn() + |> put_req_header("authorization", "Bearer " <> inactive_token) + |> AuthPlug.call([]) + + assert conn.status == 401 + assert conn.halted + end + + test "should be halted and have unauthorized status if no token was passed" do + conn = AuthPlug.call(build_conn(), []) + + assert conn.status == 401 + assert conn.halted + end + end +end diff --git a/test/wanda_web/auth/client/auth_client_http_test.exs b/test/wanda_web/auth/client/auth_client_http_test.exs new file mode 100644 index 000000000..74101e86c --- /dev/null +++ b/test/wanda_web/auth/client/auth_client_http_test.exs @@ -0,0 +1,54 @@ +defmodule WandaWeb.Auth.Client.HTTPClientTest do + use ExUnit.Case, async: true + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + alias WandaWeb.Auth.Client.HttpClient, as: AuthHttpClient + + @introspection_url "http://localhost:4000/api/session/token/introspect" + + @default_opts [url: @introspection_url, method: "post"] + + setup_all do + HTTPoison.start() + :ok + end + + describe "token introspection" do + test "returns error on non successful responses" do + for status_code <- [400, 401, 403, 404, 500] do + use_cassette :stub, stub_request(status_code: status_code) do + assert {:error, :unable_to_get_introspect_response} = + AuthHttpClient.introspect_token("token") + end + end + end + + test "returns error the decoding failure" do + use_cassette :stub, stub_request(body: "invalid_json") do + assert {:error, :cannot_decode_introspect_response} = + AuthHttpClient.introspect_token("token") + end + + use_cassette :stub, stub_request(body: "{\"active\": false}") do + assert {:ok, %{active: false}} = AuthHttpClient.introspect_token("token") + end + end + + test "returns the token introspection result" do + use_cassette :stub, + stub_request( + body: + "{\"active\": true, \"sub\": 1, \"abilities\": [{\"name\": \"foo\", \"resource\": \"bar\"}]}" + ) do + assert {:ok, %{active: true, sub: 1, abilities: [%{name: "foo", resource: "bar"}]}} == + AuthHttpClient.introspect_token("token") + end + + use_cassette :stub, stub_request(body: "{\"active\": false}") do + assert {:ok, %{active: false}} = AuthHttpClient.introspect_token("token") + end + end + end + + defp stub_request(opts), do: @default_opts ++ opts +end diff --git a/test/wanda_web/auth/jwt_auth_plug_test.exs b/test/wanda_web/auth/jwt_auth_plug_test.exs deleted file mode 100644 index 8c463d453..000000000 --- a/test/wanda_web/auth/jwt_auth_plug_test.exs +++ /dev/null @@ -1,119 +0,0 @@ -defmodule WandaWeb.Auth.JWTAuthPlugTest do - use WandaWeb.ConnCase, async: false - - import Mox - - alias WandaWeb.Auth.AccessToken - alias WandaWeb.Auth.JWTAuthPlug - - setup [:set_mox_from_context, :verify_on_exit!] - - describe "call/2" do - setup do - stub( - Joken.CurrentTime.Mock, - :current_time, - fn -> - 1_671_715_992 - end - ) - - :ok - end - - test "should return the connection with the user related information" do - jwt = - AccessToken.generate_and_sign!(%{ - "sub" => 1, - "abilities" => [ - %{ - "name" => "foo", - "resource" => "bar" - } - ] - }) - - conn = - build_conn() - |> put_req_header("authorization", "Bearer " <> jwt) - |> JWTAuthPlug.call([]) - - refute conn.halted - assert conn.private.user_id == 1 - - assert conn.private.abilities == [ - %{ - name: "foo", - resource: "bar" - } - ] - end - - test "should return the connection with the empty user abilities" do - jwt = - AccessToken.generate_and_sign!(%{ - "sub" => 1, - "abilities" => [] - }) - - conn = - build_conn() - |> put_req_header("authorization", "Bearer " <> jwt) - |> JWTAuthPlug.call([]) - - assert conn.private.abilities == [] - end - - test "should return the connection with empty user abilities when malformed ones were found" do - unrecognizable_abilities = [nil, %{}, "foo", 1, 1.0, true, false] - - for unsupported_ability <- unrecognizable_abilities do - jwt = - AccessToken.generate_and_sign!(%{ - "sub" => 1, - "abilities" => unsupported_ability - }) - - conn = - build_conn() - |> put_req_header("authorization", "Bearer " <> jwt) - |> JWTAuthPlug.call([]) - - assert conn.private.abilities == [] - end - end - - test "should be halted and have unauthorized status if the token is expired" do - expired_jwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0cmVudG8tcHJvamVjdCIsImV4cCI6MTY3MTY0MjQxNCwiaWF0IjoxNjcxNjQxODE0LCJpc3MiOiJodHRwczovL2dpdGh1Yi5jb20vdHJlbnRvLXByb2plY3Qvd2ViIiwianRpIjoiMnNwaTFvbmxxbml1ZnE5dnVrMDAwMG9hIiwibmJmIjoxNjcxNjQxODE0LCJzdWIiOjEsInR5cCI6IkJlYXJlciJ9.oub6_NsHcVIyd0de14Lzk3SuCMMgr8O-sSWLr7Gxcp8" - - conn = - build_conn() - |> put_req_header("authorization", "Bearer " <> expired_jwt) - |> JWTAuthPlug.call([]) - - assert conn.status == 401 - assert conn.halted - end - - test "should be halted and have unauthorized status if an invalid token was passed" do - invalid_jwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0cmVudG8tcHJvamVjdCIsImV4cCI6MTY3MTU1NjY5MiwiaWF0IjoxNjcxNTQ5NDkyLCJpc3MiOiJodHRwczovL2dpdGh1Yi5jb20vdHJlbnRvLXByb2plY3Qvd2ViIiwianRpIjoiMnNwOGlxMmkxNnRlbHNycWE4MDAwMWM4IiwibmJmIjoxNjcxNTQ5NDkyLCJzdWIiOjF9.PRqQgJkfxrusFtvkwk-2utMNde0TZN9zcx7ncmVxvk8" - - conn = - build_conn() - |> put_req_header("authorization", "Bearer " <> invalid_jwt) - |> JWTAuthPlug.call([]) - - assert conn.status == 401 - assert conn.halted - end - - test "should be halted and have unauthorized status if no token was passed" do - conn = JWTAuthPlug.call(build_conn(), []) - - assert conn.status == 401 - assert conn.halted - end - end -end