From 25c2c97f002871a25dccefdf20e40a82f1070491 Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sat, 8 Apr 2023 20:39:37 +0200 Subject: [PATCH 1/2] feat(api): add userinfo controller --- .../lib/ex_fleet_yards_api/router.ex | 6 +++ .../routes/userinfo_controller.ex | 40 +++++++++++++++++++ .../routes/userinfo_json.ex | 7 ++++ 3 files changed, 53 insertions(+) create mode 100644 apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex create mode 100644 apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_json.ex diff --git a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/router.ex b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/router.ex index cb7662e3..87c2a7d4 100644 --- a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/router.ex +++ b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/router.ex @@ -105,6 +105,12 @@ defmodule ExFleetYardsApi.Router do delete "/delete-account", RegisterController, :delete end end + + scope "/openid/userinfo" do + pipe_through :require_authenticated + + get "/", UserinfoController, :userinfo + end end # scope "/v2", ExFleetYardsApi do diff --git a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex new file mode 100644 index 00000000..799241b9 --- /dev/null +++ b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex @@ -0,0 +1,40 @@ +defmodule ExFleetYardsApi.Routes.UserinfoController do + use ExFleetYardsApi, :controller + + @behaviour Boruta.Openid.UserinfoApplication + + plug :put_view, ExFleetYardsApi.Routes.UserinfoJson + plug(:authorize, ["openid"] when action in [:userinfo]) + + tags ["user"] + + def openid_module, do: Application.get_env(:ex_fleet_yards_auth, :openid_module, Boruta.Openid) + + operation :userinfo, + summary: "Get user info", + responses: [ + ok: {"User", "application/json", ExFleetYardsApi.Schemas.Single.User}, + unauthorized: {"Error", "application/json", Error} + ], + security: [%{"authorization" => ["openid"]}] + + def userinfo(conn, _params) do + openid_module().userinfo(conn, __MODULE__) + end + + @impl Boruta.Openid.UserinfoApplication + def userinfo_fetched(conn, userinfo) do + conn + |> render(:userinfo, userinfo: userinfo) + end + + @impl Boruta.Openid.UserinfoApplication + def unauthorized(conn, error) do + conn + |> put_resp_header( + "www-authenticate", + "error=\"#{error.error}\", error_description=\"#{error.error_description}\"" + ) + |> send_resp(:unauthorized, "") + end +end diff --git a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_json.ex b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_json.ex new file mode 100644 index 00000000..1b7fbace --- /dev/null +++ b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_json.ex @@ -0,0 +1,7 @@ +defmodule ExFleetYardsApi.Routes.UserinfoJson do + use ExFleetYardsApi, :json + + def userinfo(%{userinfo: userinfo}) do + userinfo + end +end From dabefc0a8c9934b732c1d2d9346588e40eac03cd Mon Sep 17 00:00:00 2001 From: Finn Behrens Date: Sun, 9 Apr 2023 12:10:23 +0200 Subject: [PATCH 2/2] chore(api): add userinfo test --- .../ex_fleet_yards/oauth/resource_owners.ex | 2 +- .../routes/userinfo_controller.ex | 2 +- .../lib/ex_fleet_yards_api/schemas/single.ex | 18 ++++++++++++ .../routes/userinfo_test.exs | 29 +++++++++++++++++++ apps/ex_fleet_yards_api/test/test_helper.exs | 2 ++ .../openid/configuration_controller.ex | 15 ++++++++++ apps/ex_fleet_yards_auth/mix.exs | 7 ++++- 7 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 apps/ex_fleet_yards_api/test/ex_fleet_yards_api/routes/userinfo_test.exs diff --git a/apps/ex_fleet_yards/lib/ex_fleet_yards/oauth/resource_owners.ex b/apps/ex_fleet_yards/lib/ex_fleet_yards/oauth/resource_owners.ex index 6f1d42ab..e6dd0c73 100644 --- a/apps/ex_fleet_yards/lib/ex_fleet_yards/oauth/resource_owners.ex +++ b/apps/ex_fleet_yards/lib/ex_fleet_yards/oauth/resource_owners.ex @@ -61,7 +61,7 @@ defmodule ExFleetYards.Oauth.ResourceOwners do [ {"nickname", user.username}, {"hangar_updated_at", user.hangar_updated_at}, - {"public_hangar", user.public_hangar} + {"publicHangar", user.public_hangar} | add_claims(resource_owner, user, scopes) ] end diff --git a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex index 799241b9..cb4d0675 100644 --- a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex +++ b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/routes/userinfo_controller.ex @@ -13,7 +13,7 @@ defmodule ExFleetYardsApi.Routes.UserinfoController do operation :userinfo, summary: "Get user info", responses: [ - ok: {"User", "application/json", ExFleetYardsApi.Schemas.Single.User}, + ok: {"Userinfo", "application/json", ExFleetYardsApi.Schemas.Single.Userinfo}, unauthorized: {"Error", "application/json", Error} ], security: [%{"authorization" => ["openid"]}] diff --git a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/schemas/single.ex b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/schemas/single.ex index 0355a45a..203dd9d4 100644 --- a/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/schemas/single.ex +++ b/apps/ex_fleet_yards_api/lib/ex_fleet_yards_api/schemas/single.ex @@ -717,6 +717,24 @@ defmodule ExFleetYardsApi.Schemas.Single do }) end + defmodule Userinfo do + @moduledoc "Userinfo Schema" + require OpenApiSpex + + OpenApiSpex.schema(%{ + description: "Userinfo Schema", + type: :object, + properties: %{ + sub: %Schema{type: :string, format: :uuid}, + email: %Schema{type: :string, format: :email}, + hangar_updated_at: %Schema{type: :string, format: :"date-time"}, + nickname: %Schema{type: :string}, + publicHangar: %Schema{type: :boolean} + }, + required: [:sub] + }) + end + defmodule Version do require OpenApiSpex diff --git a/apps/ex_fleet_yards_api/test/ex_fleet_yards_api/routes/userinfo_test.exs b/apps/ex_fleet_yards_api/test/ex_fleet_yards_api/routes/userinfo_test.exs new file mode 100644 index 00000000..e0094b84 --- /dev/null +++ b/apps/ex_fleet_yards_api/test/ex_fleet_yards_api/routes/userinfo_test.exs @@ -0,0 +1,29 @@ +defmodule ExFleetYardsApi.Routes.UserinfoTest do + use ExFleetYardsApi.ConnCase, async: true + use ExFleetYardsApi.Mox + + import OpenApiSpex.TestAssertions + + describe "userinfo" do + test "return userinfo response", %{conn: conn, api_spec: spec} do + login_user("testuser", ["openid"]) + + userinfo = %{ + "sub" => SecureRandom.uuid() + } + + Boruta.OpenidMock + |> expect(:userinfo, fn conn, module -> + module.userinfo_fetched(conn, userinfo) + end) + + json = + conn + |> get("/v2/openid/userinfo") + |> json_response(200) + + assert_schema json, "Userinfo", spec + assert json == userinfo + end + end +end diff --git a/apps/ex_fleet_yards_api/test/test_helper.exs b/apps/ex_fleet_yards_api/test/test_helper.exs index 1143b8b6..c4630e79 100644 --- a/apps/ex_fleet_yards_api/test/test_helper.exs +++ b/apps/ex_fleet_yards_api/test/test_helper.exs @@ -2,3 +2,5 @@ ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(ExFleetYards.Repo, :manual) Mox.defmock(ExFleetYardsApi.Plugs.AuthorizationMock, for: ExFleetYardsApi.Plugs.Authorization) +Mox.defmock(Boruta.OauthMock, for: Boruta.OauthModule) +Mox.defmock(Boruta.OpenidMock, for: Boruta.OpenidModule) diff --git a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/openid/configuration_controller.ex b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/openid/configuration_controller.ex index 5c6a463f..4b45e457 100644 --- a/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/openid/configuration_controller.ex +++ b/apps/ex_fleet_yards_auth/lib/ex_fleet_yards_auth/controllers/openid/configuration_controller.ex @@ -12,6 +12,7 @@ defmodule ExFleetYardsAuth.Openid.ConfigurationController do issuer: issuer, auth_endpoint: base_url <> ~p"/oauth/authorize", token_endpoint: base_url <> ~p"/oauth/token", + userinfo_endpoint: userinfo_endpoint(), jwks_url: base_url <> ~p"/openid/certs", scopes_supported: scope_list(), response_types_supported: ["id_token", "code id_token", "id_token token"], @@ -28,4 +29,18 @@ defmodule ExFleetYardsAuth.Openid.ConfigurationController do ExFleetYards.Scopes.scope_list() |> Enum.map(fn {scope, _} -> scope end) end + + defp api_host do + case Code.ensure_compiled(ExFleetYardsApi.Endpoint) do + {:module, _} -> + ExFleetYardsApi.Endpoint.host() + + {:error, _} -> + "https://" <> Application.get_env(:ex_fleet_yards_auth, :api_domain) + end + end + + defp userinfo_endpoint do + api_host() <> "/v2/openid/userinfo" + end end diff --git a/apps/ex_fleet_yards_auth/mix.exs b/apps/ex_fleet_yards_auth/mix.exs index dcd0ca77..230c3fac 100644 --- a/apps/ex_fleet_yards_auth/mix.exs +++ b/apps/ex_fleet_yards_auth/mix.exs @@ -14,7 +14,8 @@ defmodule ExFleetYardsAuth.MixProject do compilers: [] ++ Mix.compilers(), start_permanent: Mix.env() == :prod, aliases: aliases(), - deps: deps() + deps: deps(), + xref: xref() ] end @@ -55,6 +56,10 @@ defmodule ExFleetYardsAuth.MixProject do ] end + defp xref do + [exclude: [ExFleetYardsApi.Endpoint]] + end + def aliases do [ "assets.deploy": ["tailwind auth --minify", "esbuild auth --minify", "phx.digest"]