From 6c217d3e392ce5c713381791e61c7e30294034cf Mon Sep 17 00:00:00 2001 From: Tommaso Patrizi Date: Tue, 24 Sep 2024 16:02:30 +0200 Subject: [PATCH] added token introspection --- lib/assent/strategies/zitadel.ex | 48 ++++++++++++++++++++++--- test/assent/strategies/zitadel_test.exs | 46 ++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/lib/assent/strategies/zitadel.ex b/lib/assent/strategies/zitadel.ex index 2c4cdd5..a011c4c 100644 --- a/lib/assent/strategies/zitadel.ex +++ b/lib/assent/strategies/zitadel.ex @@ -110,6 +110,20 @@ defmodule Assent.Strategy.Zitadel do end end + defp jwt_authentication_params(config) do + with {:ok, token} <- gen_client_secret(config) do + headers = [] + + body = [ + scope: "openid", + assertion: token, + grant_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + ] + + {:ok, headers, body} + end + end + defp process_access_token_response( {:ok, %HTTPResponse{status: status, body: %{"access_token" => _} = token}} ) @@ -127,20 +141,46 @@ defmodule Assent.Strategy.Zitadel do defp process_response({:error, error}), do: {:error, error} - defp jwt_authentication_params(config) do + @doc """ + Introspects a given tokein with zitadel with JWT + """ + @spec introspect_token(Config.t(), binary()) :: {:ok, map()} | {:error, term()} + def introspect_token(config, access_token) do + introspect_url = Config.get(config, :introspect_url, "/oauth/v2/introspect") + + with {:ok, base_url} <- Config.__base_url__(config), + {:ok, auth_headers, params} <- jwt_introspection_params(config, access_token) do + headers = [{"content-type", "application/x-www-form-urlencoded"}] ++ auth_headers + url = Helpers.to_url(base_url, introspect_url) + body = URI.encode_query(params) + + :post + |> Helpers.request(url, body, headers, config) + |> process_introspect_token_response() + end + end + + defp jwt_introspection_params(config, access_token) do with {:ok, token} <- gen_client_secret(config) do headers = [] body = [ - scope: "openid", - assertion: token, - grant_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + client_assertion: token, + client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", + token: access_token ] {:ok, headers, body} end end + defp process_introspect_token_response( + {:ok, %HTTPResponse{status: status, body: %{"active" => _} = token}} + ) + when status in [200, 201] do + {:ok, token} + end + @jwt_expiration_seconds 3600 defp gen_client_secret(config) do diff --git a/test/assent/strategies/zitadel_test.exs b/test/assent/strategies/zitadel_test.exs index 5103030..3b6569e 100644 --- a/test/assent/strategies/zitadel_test.exs +++ b/test/assent/strategies/zitadel_test.exs @@ -114,6 +114,33 @@ defmodule Assent.Strategy.ZitadelTest do assert {:ok, %{"access_token" => "access_token"}} == Zitadel.authenticate_api(config) end + test "introspect_token/2", %{config: config} do + config = + Keyword.merge(config, + client_id: @client_id, + resource_id: @resource_id, + private_key: @private_key, + private_key_id: @private_key_id + ) + + expect_introspect_token_request( + response: %{ + active: true, + client_id: @client_id, + scope: "openid email profile", + username: "username@example.com" + } + ) + + assert {:ok, + %{ + "active" => true, + "client_id" => "3425235252@nameofproject", + "scope" => "openid email profile", + "username" => "username@example.com" + }} == Zitadel.introspect_token(config, "access_token") + end + @spec expect_api_access_token_request(Keyword.t(), function() | nil) :: :ok defp expect_api_access_token_request(opts \\ [], assert_fn \\ nil) do access_token = Keyword.get(opts, :access_token, "access_token") @@ -134,6 +161,25 @@ defmodule Assent.Strategy.ZitadelTest do ) end + @spec expect_introspect_token_request(Keyword.t(), function() | nil) :: :ok + defp expect_introspect_token_request(opts \\ [], assert_fn \\ nil) do + resp_body = Keyword.get(opts, :response) + uri = Keyword.get(opts, :uri, "/oauth/v2/introspect") + status_code = Keyword.get(opts, :status_code, 200) + + TestServer.add(uri, + via: :post, + to: fn conn -> + {:ok, body, _conn} = Plug.Conn.read_body(conn, []) + params = URI.decode_query(body) + + if assert_fn, do: assert_fn.(conn, params) + + send_json_resp(conn, resp_body, status_code) + end + ) + end + defp send_json_resp(conn, body, status_code) do conn |> Conn.put_resp_content_type("application/json")