From 1ffdfdfd63fe253a49905783eb5bd9f4dc74b052 Mon Sep 17 00:00:00 2001 From: Mathias Wingert Date: Fri, 24 Oct 2025 12:48:02 +0200 Subject: [PATCH] Allow redirect_uri, client_id, client_secret to be set through a fn with Plug.Conn as param For use cases where the redirect_uri needs to be set dynamically depending on the current Plug.Conn. For example when the redirect_uri needs to contain the tenant as subdomain. ```Elixir plug Oidcc.Plug.AuthorizationCallback, provider: SampleApp.GoogleOpenIdConfigurationProvider, redirect_uri: &__MODULE__.get_callback_uri/1 def get_callback_uri(%Plug.Conn{} = conn) do tenant = conn.assigns.tenant "https://#{tenant}.localhost:4000/oidcc/callback" end ``` --- lib/oidcc/plug/authorization_callback.ex | 10 ++-- lib/oidcc/plug/authorize.ex | 10 ++-- lib/oidcc/plug/config.ex | 14 ++++-- lib/oidcc/plug/introspect_token.ex | 4 +- lib/oidcc/plug/load_userinfo.ex | 10 ++-- lib/oidcc/plug/utils.ex | 8 ++-- .../plug/authorization_callback_test.exs | 47 +++++++++++++++++++ 7 files changed, 78 insertions(+), 25 deletions(-) diff --git a/lib/oidcc/plug/authorization_callback.ex b/lib/oidcc/plug/authorization_callback.ex index e3149bb..cadce6a 100644 --- a/lib/oidcc/plug/authorization_callback.ex +++ b/lib/oidcc/plug/authorization_callback.ex @@ -80,7 +80,7 @@ defmodule Oidcc.Plug.AuthorizationCallback do @behaviour Plug - import Oidcc.Plug.Config, only: [evaluate_config: 1] + import Oidcc.Plug.Config, only: [evaluate_config: 2] import Plug.Conn, only: [get_session: 2, delete_session: 2, put_private: 3, get_req_header: 2] @@ -118,11 +118,11 @@ defmodule Oidcc.Plug.AuthorizationCallback do @type opts() :: [ provider: GenServer.name() | nil, client_store: module() | nil, - client_id: String.t() | (-> String.t()) | nil, - client_secret: String.t() | (-> String.t()) | nil, + client_id: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()) | nil, + client_secret: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()) | nil, client_context_opts: :oidcc_client_context.opts() | (-> :oidcc_client_context.opts()), client_profile_opts: :oidcc_profile.opts(), - redirect_uri: String.t() | (-> String.t()), + redirect_uri: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), check_useragent: boolean(), check_peer_ip: boolean(), retrieve_userinfo: boolean(), @@ -160,7 +160,7 @@ defmodule Oidcc.Plug.AuthorizationCallback do @impl Plug def call(%Plug.Conn{params: params, body_params: body_params} = conn, opts) do - redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config() + redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config(conn) client_profile_opts = Keyword.get(opts, :client_profile_opts, %{profiles: []}) params = Map.merge(params, body_params) diff --git a/lib/oidcc/plug/authorize.ex b/lib/oidcc/plug/authorize.ex index d6a99f2..c1b40d7 100644 --- a/lib/oidcc/plug/authorize.ex +++ b/lib/oidcc/plug/authorize.ex @@ -27,7 +27,7 @@ defmodule Oidcc.Plug.Authorize do @behaviour Plug - import Oidcc.Plug.Config, only: [evaluate_config: 1] + import Oidcc.Plug.Config, only: [evaluate_config: 2] import Plug.Conn, only: [send_resp: 3, put_resp_header: 3, put_session: 3, get_req_header: 2] @@ -71,12 +71,12 @@ defmodule Oidcc.Plug.Authorize do @typedoc since: "0.1.0" @type opts :: [ scopes: :oidcc_scope.scopes(), - redirect_uri: String.t() | (-> String.t()), + redirect_uri: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), url_extension: :oidcc_http_util.query_params(), provider: GenServer.name() | nil, client_store: module() | nil, - client_id: String.t() | (-> String.t()) | nil, - client_secret: String.t() | (-> String.t()) | nil, + client_id: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()) | nil, + client_secret: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()) | nil, client_context_opts: :oidcc_client_context.opts() | (-> :oidcc_client_context.opts()) | nil, client_profile_opts: :oidcc_profile.opts() ] @@ -100,7 +100,7 @@ defmodule Oidcc.Plug.Authorize do @impl Plug def call(%Plug.Conn{params: params} = conn, opts) do - redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config() + redirect_uri = opts |> Keyword.fetch!(:redirect_uri) |> evaluate_config(conn) client_profile_opts = Keyword.get(opts, :client_profile_opts, %{profiles: []}) state = Map.get(params, "state", :undefined) diff --git a/lib/oidcc/plug/config.ex b/lib/oidcc/plug/config.ex index c281c40..e872d56 100644 --- a/lib/oidcc/plug/config.ex +++ b/lib/oidcc/plug/config.ex @@ -1,8 +1,14 @@ defmodule Oidcc.Plug.Config do @moduledoc false - @spec evaluate_config(config :: value | (-> value)) :: value when value: term() - def evaluate_config(config) - def evaluate_config(config) when is_function(config, 0), do: config.() - def evaluate_config(config), do: config + @spec evaluate_config(config :: value | (-> value) | (Plug.Conn.t() -> value), Plug.Conn.t()) :: value + when value: term() + def evaluate_config(config, conn) + def evaluate_config(config, _conn) when is_function(config, 0), do: config.() + def evaluate_config(config, conn) when is_function(config, 1), do: config.(conn) + + def evaluate_config(config, _conn) when is_function(config), + do: raise(ArgumentError, "Config function must have arity 0 or 1") + + def evaluate_config(config, _conn), do: config end diff --git a/lib/oidcc/plug/introspect_token.ex b/lib/oidcc/plug/introspect_token.ex index 6da2b15..a3c3314 100644 --- a/lib/oidcc/plug/introspect_token.ex +++ b/lib/oidcc/plug/introspect_token.ex @@ -55,8 +55,8 @@ defmodule Oidcc.Plug.IntrospectToken do @typedoc since: "0.1.0" @type opts :: [ provider: GenServer.name(), - client_id: String.t() | (-> String.t()), - client_secret: String.t() | (-> String.t()), + client_id: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), + client_secret: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), token_introspection_opts: :oidcc_token_introspection.opts(), send_inactive_token_response: (conn :: Plug.Conn.t(), introspection :: TokenIntrospection.t() -> Plug.Conn.t()), diff --git a/lib/oidcc/plug/load_userinfo.ex b/lib/oidcc/plug/load_userinfo.ex index ac83de1..a1d2d9c 100644 --- a/lib/oidcc/plug/load_userinfo.ex +++ b/lib/oidcc/plug/load_userinfo.ex @@ -30,7 +30,7 @@ defmodule Oidcc.Plug.LoadUserinfo do @behaviour Plug - import Oidcc.Plug.Config, only: [evaluate_config: 1] + import Oidcc.Plug.Config, only: [evaluate_config: 2] import Plug.Conn, only: [put_private: 3, halt: 1, send_resp: 3] alias Oidcc.Plug.ExtractAuthorization @@ -50,8 +50,8 @@ defmodule Oidcc.Plug.LoadUserinfo do @typedoc since: "0.1.0" @type opts :: [ provider: GenServer.name(), - client_id: String.t() | (-> String.t()), - client_secret: String.t() | (-> String.t()), + client_id: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), + client_secret: String.t() | (-> String.t()) | (Plug.Conn.t() -> String.t()), userinfo_retrieve_opts: :oidcc_userinfo.retrieve_opts(), send_inactive_token_response: (conn :: Plug.Conn.t() -> Plug.Conn.t()), cache: Oidcc.Plug.Cache.t() @@ -88,8 +88,8 @@ defmodule Oidcc.Plug.LoadUserinfo do def call(%Plug.Conn{private: %{ExtractAuthorization => access_token}} = conn, opts) do provider = Keyword.fetch!(opts, :provider) - client_id = opts |> Keyword.fetch!(:client_id) |> evaluate_config() - client_secret = opts |> Keyword.fetch!(:client_secret) |> evaluate_config() + client_id = opts |> Keyword.fetch!(:client_id) |> evaluate_config(conn) + client_secret = opts |> Keyword.fetch!(:client_secret) |> evaluate_config(conn) userinfo_retrieve_opts = opts diff --git a/lib/oidcc/plug/utils.ex b/lib/oidcc/plug/utils.ex index 7061a55..af2cc67 100644 --- a/lib/oidcc/plug/utils.ex +++ b/lib/oidcc/plug/utils.ex @@ -1,7 +1,7 @@ defmodule Oidcc.Plug.Utils do @moduledoc false - import Oidcc.Plug.Config, only: [evaluate_config: 1] + import Oidcc.Plug.Config, only: [evaluate_config: 2] alias Oidcc.ClientContext @@ -15,9 +15,9 @@ defmodule Oidcc.Plug.Utils do client_store.get_client_context(conn) else provider = Keyword.get(opts, :provider) - client_id = opts |> Keyword.get(:client_id) |> evaluate_config() - client_secret = opts |> Keyword.get(:client_secret) |> evaluate_config() - client_context_opts = opts |> Keyword.get(:client_context_opts, %{}) |> evaluate_config() + client_id = opts |> Keyword.get(:client_id) |> evaluate_config(conn) + client_secret = opts |> Keyword.get(:client_secret) |> evaluate_config(conn) + client_context_opts = opts |> Keyword.get(:client_context_opts, %{}) |> evaluate_config(conn) ClientContext.from_configuration_worker( provider, diff --git a/test/oidcc/plug/authorization_callback_test.exs b/test/oidcc/plug/authorization_callback_test.exs index d385190..83384ec 100644 --- a/test/oidcc/plug/authorization_callback_test.exs +++ b/test/oidcc/plug/authorization_callback_test.exs @@ -92,6 +92,53 @@ defmodule Oidcc.Plug.AuthorizationCallbackTest do end end + test "successful retrieve with dynamic config" do + with_mocks [ + {Oidcc.Token, [], + retrieve: fn "code", + _client_context, + %{ + redirect_uri: "http://localhost:8080/oidc/return", + nonce: _nonce, + refresh_jwks: _refresh_fun + } -> + {:ok, :token} + end}, + {Oidcc.Userinfo, [], + retrieve: fn :token, _client_context, %{} -> + {:ok, %{"sub" => "sub"}} + end} + ] do + opts = + AuthorizationCallback.init( + provider: ProviderName, + client_id: fn %Plug.Conn{} -> "client_id" end, + client_secret: fn %Plug.Conn{} -> "client_secret" end, + redirect_uri: fn %Plug.Conn{} -> "http://localhost:8080/oidc/return" end + ) + + assert %{ + halted: false, + private: %{ + AuthorizationCallback => {:ok, {:token, %{"sub" => "sub"}}} + } + } = + "get" + |> conn("/", %{"code" => "code"}) + |> Plug.Test.init_test_session(%{ + Authorize.get_session_name() => %{ + nonce: "nonce", + peer_ip: {127, 0, 0, 1}, + useragent: "useragent", + pkce_verifier: "pkce_verifier", + state_verifier: 0 + } + }) + |> put_req_header("user-agent", "useragent") + |> AuthorizationCallback.call(opts) + end + end + test_with_mock "successful retrieve without userinfo", %{}, Oidcc.Token, [], retrieve: fn "code", _client_context, %{redirect_uri: "http://localhost:8080/oidc/return", nonce: _nonce} -> {:ok, :token}