diff --git a/include/oidcc_client_registration.hrl b/include/oidcc_client_registration.hrl
index 8dd3377..3724c2a 100644
--- a/include/oidcc_client_registration.hrl
+++ b/include/oidcc_client_registration.hrl
@@ -1,37 +1,70 @@
-ifndef(OIDCC_CLIENT_REGISTRATION_HRL).
%% @see https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
+%% @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata
-record(oidcc_client_registration, {
+ %% OpenID Connect Dynamic Client Registration 1.0
redirect_uris :: [uri_string:uri_string()],
+ %% OpenID Connect Dynamic Client Registration 1.0
response_types = undefined :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
grant_types = undefined :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
application_type = web :: web | native,
+ %% OpenID Connect Dynamic Client Registration 1.0
contacts = undefined :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
client_name = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
logo_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
client_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
policy_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
tos_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
jwks = undefined :: jose_jwk:key() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
jwks_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
sector_identifier_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
subject_type = undefined :: pairwise | public | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_signed_response_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_encrypted_response_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_encrypted_response_enc = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_signed_response_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_encrypted_response_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_encrypted_response_enc = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_signing_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_encryption_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_encryption_enc = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
token_endpoint_auth_method = <<"client_secret_basic">> :: binary(),
+ %% OpenID Connect Dynamic Client Registration 1.0
token_endpoint_auth_signing_alg = undefined :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
default_max_age = undefined :: pos_integer() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
require_auth_time = false :: boolean(),
+ %% OpenID Connect Dynamic Client Registration 1.0
default_acr_values = undefined :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
initiate_login_uri = undefined :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_uris = undefined :: [uri_string:uri_string()] | undefined,
+ %% OpenID Connect RP-Initiated Logout 1.0
+ post_logout_redirect_uris = undefined :: [uri_string:uri_string()] | undefined,
%% Unknown Fields
extra_fields = #{} :: #{binary() => term()}
}).
diff --git a/include/oidcc_provider_configuration.hrl b/include/oidcc_provider_configuration.hrl
index ee896af..e32886c 100644
--- a/include/oidcc_provider_configuration.hrl
+++ b/include/oidcc_provider_configuration.hrl
@@ -2,6 +2,7 @@
%% @see https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
%% @see https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1
+%% @see https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata
-record(oidcc_provider_configuration,
%% OpenID Connect Discovery 1.0 / OAuth 2.0 Discovery (draft-jones-oauth-discovery-01)
{
@@ -90,6 +91,8 @@
[binary()] | undefined,
%% OAuth 2.0 Discovery (draft-jones-oauth-discovery-01)
code_challenge_methods_supported = undefined :: [binary()] | undefined,
+ %% OpenID Connect RP-Initiated Logout 1.0
+ end_session_endpoint = undefined :: uri_string:uri_string() | undefined,
%% Unknown Fields
extra_fields = #{} :: #{binary() => term()}
}
diff --git a/lib/oidcc.ex b/lib/oidcc.ex
index dd0590b..93b08eb 100644
--- a/lib/oidcc.ex
+++ b/lib/oidcc.ex
@@ -352,4 +352,61 @@ defmodule Oidcc do
opts
)
|> Oidcc.Token.normalize_token_response()
+
+ @doc """
+ Create Initiate URI for Relaying Party initated Logout
+
+ See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
+
+ ## Examples
+
+ iex> {:ok, pid} =
+ ...> Oidcc.ProviderConfiguration.Worker.start_link(%{
+ ...> issuer: "https://erlef-test-w4a8z2.zitadel.cloud"
+ ...> })
+ ...>
+ ...> # Get access_token from Oidcc.Token.retrieve/3
+ ...> token = "token"
+ ...>
+ ...> {:ok, _redirect_uri} = Oidcc.initiate_logout_url(
+ ...> token,
+ ...> pid,
+ ...> "client_id",
+ ...> "client_secret"
+ ...> )
+
+ """
+ @doc since: "3.0.0"
+ @spec initiate_logout_url(
+ token :: id_token | Oidcc.Token.t() | :undefined,
+ provider_configuration_name :: GenServer.name(),
+ client_id :: String.t(),
+ client_secret :: String.t(),
+ opts :: :oidcc_logout.initiate_url_opts() | :oidcc_client_context.opts()
+ ) ::
+ {:ok, :uri_string.uri_string()}
+ | {:error, :oidcc_client_context.error() | :oidcc_logout.error()}
+ when id_token: String.t()
+ def initiate_logout_url(
+ token,
+ provider_configuration_name,
+ client_id,
+ client_secret,
+ opts \\ %{}
+ ) do
+ token =
+ case token do
+ %Oidcc.Token{} = token -> Oidcc.Token.struct_to_record(token)
+ token when is_binary(token) -> token
+ :undefined -> :undefined
+ end
+
+ :oidcc.initiate_logout_url(
+ token,
+ provider_configuration_name,
+ client_id,
+ client_secret,
+ opts
+ )
+ end
end
diff --git a/lib/oidcc/client_registration.ex b/lib/oidcc/client_registration.ex
index 70aff48..fe7eed2 100644
--- a/lib/oidcc/client_registration.ex
+++ b/lib/oidcc/client_registration.ex
@@ -44,7 +44,8 @@ defmodule Oidcc.ClientRegistration do
@typedoc """
Client Metdata Struct
- See https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata
+ See https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata and
+ https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata
"""
@typedoc since: "3.0.0"
@type t() :: %__MODULE__{
@@ -78,6 +79,7 @@ defmodule Oidcc.ClientRegistration do
default_acr_values: [String.t()] | :undefined,
initiate_login_uri: :uri_string.uri_string() | :undefined,
request_uris: [:uri_string.uri_string()] | :undefined,
+ post_logout_redirect_uris: [:uri_string.uri_string()] | :undefined,
extra_fields: %{String.t() => term()}
}
diff --git a/lib/oidcc/logout.ex b/lib/oidcc/logout.ex
new file mode 100644
index 0000000..ed9464d
--- /dev/null
+++ b/lib/oidcc/logout.ex
@@ -0,0 +1,55 @@
+defmodule Oidcc.Logout do
+ @moduledoc """
+ Logout from the OpenID Provider
+ """
+ @moduledoc since: "3.0.0"
+
+ alias Oidcc.ClientContext
+
+ @doc """
+ Initiate URI for Relaying Party initated Logout
+
+ See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout
+
+ For a high level interface using `Oidcc.ProviderConfiguration.Worker`
+ see `Oidcc.initiate_logout_url/5`.
+
+ ## Examples
+
+ iex> {:ok, pid} =
+ ...> Oidcc.ProviderConfiguration.Worker.start_link(%{
+ ...> issuer: "https://erlef-test-w4a8z2.zitadel.cloud"
+ ...> })
+ ...>
+ ...> {:ok, client_context} =
+ ...> Oidcc.ClientContext.from_configuration_worker(
+ ...> pid,
+ ...> "client_id",
+ ...> "client_secret"
+ ...> )
+ ...>
+ ...> # Get `token` from `Oidcc.retrieve_token/5`
+ ...> token = "token"
+ ...>
+ ...> {:ok, _redirect_uri} =
+ ...> Oidcc.Logout.initiate_url(
+ ...> token,
+ ...> client_context,
+ ...> %{post_logout_redirect_uri: "https://my.server/return"}
+ ...> )
+ """
+ @doc since: "3.0.0"
+ @spec initiate_url(
+ token :: id_token | Oidcc.Token.t() | :undefined,
+ client_context :: ClientContext.t(),
+ opts :: :oidcc_logout.initiate_url_opts()
+ ) ::
+ {:ok, :uri_string.uri_string()}
+ | {:error, :oidcc_logout.error()}
+ when id_token: String.t()
+ def initiate_url(token, client_context, opts \\ %{}) do
+ client_context = ClientContext.struct_to_record(client_context)
+
+ :oidcc_logout.initiate_url(token, client_context, opts)
+ end
+end
diff --git a/lib/oidcc/provider_configuration.ex b/lib/oidcc/provider_configuration.ex
index fcb92c5..f1b8a27 100644
--- a/lib/oidcc/provider_configuration.ex
+++ b/lib/oidcc/provider_configuration.ex
@@ -63,6 +63,7 @@ defmodule Oidcc.ProviderConfiguration do
For details on the fields see:
* https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
* https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1
+ * https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata
"""
@typedoc since: "3.0.0"
@type t() :: %__MODULE__{
@@ -108,6 +109,7 @@ defmodule Oidcc.ProviderConfiguration do
introspection_endpoint_auth_methods_supported: [String.t()],
introspection_endpoint_auth_signing_alg_values_supported: [String.t()] | :undefined,
code_challenge_methods_supported: [String.t()] | :undefined,
+ end_session_endpoint: :uri_string.uri_string() | :undefined,
extra_fields: %{String.t() => term()}
}
diff --git a/src/oidcc.erl b/src/oidcc.erl
index 627e38e..aa2f2ce 100644
--- a/src/oidcc.erl
+++ b/src/oidcc.erl
@@ -30,6 +30,7 @@
-export([client_credentials_token/4]).
-export([create_redirect_url/4]).
+-export([initiate_logout_url/5]).
-export([introspect_token/5]).
-export([jwt_profile_token/6]).
-export([refresh_token/5]).
@@ -427,6 +428,58 @@ client_credentials_token(ProviderConfigurationWorkerName, ClientId, ClientSecret
oidcc_token:client_credentials(ClientContext, OptsWithRefresh)
end.
+%% @doc
+%% Create Initiate URI for Relaying Party initated Logout
+%%
+%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
+%%
+%%
Examples
+%%
+%% ```
+%% %% Get `Token` from `oidcc_token`
+%%
+%% {ok, RedirectUri} =
+%% oidcc:initiate_logout_url(
+%% Token,
+%% provider_name,
+%% <<"client_id">>,
+%% <<"client_secret">>,
+%% #{post_logout_redirect_uri: <<"https://my.server/return"}
+%% ),
+%%
+%% %% RedirectUri = https://my.provider/logout?id_token_hint=IDToken&client_id=ClientId&post_logout_redirect_uri=https%3A%2F%2Fmy.server%2Freturn
+%% '''
+%% @end
+%% @since 3.0.0
+-spec initiate_logout_url(
+ Token,
+ ProviderConfigurationWorkerName,
+ ClientId,
+ ClientSecret,
+ Opts
+) ->
+ {ok, uri_string:uri_string()} | {error, oidcc_client_context:error() | oidcc_logout:error()}
+when
+ Token :: IdToken | oidcc_token:t() | undefined,
+ IdToken :: binary(),
+ ProviderConfigurationWorkerName :: gen_server:server_ref(),
+ ClientId :: binary(),
+ ClientSecret :: binary(),
+ Opts :: oidcc_logout:initiate_url_opts() | oidcc_client_context:opts().
+initiate_logout_url(Token, ProviderConfigurationWorkerName, ClientId, ClientSecret, Opts) ->
+ {ClientContextOpts, OtherOpts} = extract_client_context_opts(Opts),
+
+ maybe
+ {ok, ClientContext} ?=
+ oidcc_client_context:from_configuration_worker(
+ ProviderConfigurationWorkerName,
+ ClientId,
+ ClientSecret,
+ ClientContextOpts
+ ),
+ oidcc_logout:initiate_url(Token, ClientContext, OtherOpts)
+ end.
+
-spec maps_put_new(Key, Value, Map1) -> Map2 when
Key :: term(), Value :: term(), Map1 :: map(), Map2 :: map().
maps_put_new(Key, Value, Map) ->
diff --git a/src/oidcc_client_registration.erl b/src/oidcc_client_registration.erl
index 38f4f82..bb36c3f 100644
--- a/src/oidcc_client_registration.erl
+++ b/src/oidcc_client_registration.erl
@@ -46,42 +46,75 @@
-type t() ::
#oidcc_client_registration{
+ %% OpenID Connect Dynamic Client Registration 1.0
redirect_uris :: [uri_string:uri_string()],
+ %% OpenID Connect Dynamic Client Registration 1.0
response_types :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
grant_types :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
application_type :: web | native,
+ %% OpenID Connect Dynamic Client Registration 1.0
contacts :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
client_name :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
logo_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
client_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
policy_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
tos_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
jwks :: jose_jwk:key() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
jwks_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
sector_identifier_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
subject_type :: pairwise | public | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_signed_response_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_encrypted_response_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
id_token_encrypted_response_enc :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_signed_response_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_encrypted_response_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
userinfo_encrypted_response_enc :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_signing_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_encryption_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_object_encryption_enc :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
token_endpoint_auth_method :: erlang:binary(),
+ %% OpenID Connect Dynamic Client Registration 1.0
token_endpoint_auth_signing_alg :: binary() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
default_max_age :: pos_integer() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
require_auth_time :: boolean(),
+ %% OpenID Connect Dynamic Client Registration 1.0
default_acr_values :: [binary()] | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
initiate_login_uri :: uri_string:uri_string() | undefined,
+ %% OpenID Connect Dynamic Client Registration 1.0
request_uris :: [uri_string:uri_string()] | undefined,
+ %% OpenID Connect RP-Initiated Logout 1.0
+ post_logout_redirect_uris :: [uri_string:uri_string()] | undefined,
%% Unknown Fields
extra_fields :: #{binary() => term()}
}.
%% Record containing Client Registration Metadata
%%
-%% See [https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata]
+%% See [https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata] and
+%% [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#ClientMetadata]
%%
%% All unrecognized fields are stored in `extra_fields'.
@@ -259,6 +292,7 @@ encode(#oidcc_client_registration{
default_acr_values = DefaultAcrValues,
initiate_login_uri = InitiateLoginUri,
request_uris = RequestUris,
+ post_logout_redirect_uris = PostLogoutRedirectUris,
extra_fields = ExtraFields
}) ->
Map0 = #{
@@ -298,13 +332,14 @@ encode(#oidcc_client_registration{
default_acr_values => DefaultAcrValues,
initiate_login_uri => InitiateLoginUri,
request_uris => RequestUris,
- extra_fields => ExtraFields
+ post_logout_redirect_uris => PostLogoutRedirectUris
},
+ Map1 = maps:merge(Map0, ExtraFields),
Map = maps:filter(
fun
(_Key, undefined) -> false;
(_Key, _Value) -> true
end,
- Map0
+ Map1
),
jose:encode(Map).
diff --git a/src/oidcc_logout.erl b/src/oidcc_logout.erl
new file mode 100644
index 0000000..9d555c1
--- /dev/null
+++ b/src/oidcc_logout.erl
@@ -0,0 +1,118 @@
+%%%-------------------------------------------------------------------
+%% @doc Logout from the OpenID Provider
+%% @end
+%% @since 3.0.0
+%%%-------------------------------------------------------------------
+-module(oidcc_logout).
+
+-feature(maybe_expr, enable).
+
+-include("oidcc_client_context.hrl").
+-include("oidcc_provider_configuration.hrl").
+-include("oidcc_token.hrl").
+
+-export([initiate_url/3]).
+
+-export_type([error/0]).
+-export_type([initiate_url_opts/0]).
+
+-type error() :: end_session_endpoint_not_supported.
+
+-type initiate_url_opts() :: #{
+ logout_hint => binary(),
+ post_logout_redirect_uri => uri_string:uri_string(),
+ state => binary(),
+ ui_locales => binary(),
+ extra_query_params => oidcc_http_util:query_params()
+}.
+%% Configure Relaying Party initiated Logout URI
+%%
+%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
+%%
+%% Parameters
+%%
+%%
+%% - `logout_hint' - logout_hint to pass to the provider
+%% - `post_logout_redirect_uri' - Post Logout Redirect URI to pass to the
+%% provider
+%% - `state' - state to pass to the provider
+%% - `ui_locales' - UI locales to pass to the provider
+%% - `extra_query_params' - extra query params to add to the uri
+%%
+
+%% @doc
+%% Initiate URI for Relaying Party initated Logout
+%%
+%% See [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout]
+%%
+%% For a high level interface using {@link oidcc_provider_configuration_worker}
+%% see {@link oidcc:initiate_logout_url/5}.
+%%
+%% Examples
+%%
+%% ```
+%% {ok, ClientContext} = oidcc_client_context:from_configuration_worker(
+%% provider_name,
+%% <<"client_id">>,
+%% <<"client_secret">>
+%% ),
+%%
+%% %% Get `Token` from `oidcc_token`
+%%
+%% {ok, RedirectUri} =
+%% oidcc_logout:initiate_url(
+%% Token,
+%% ClientContext,
+%% #{post_logout_redirect_uri: <<"https://my.server/return"}
+%% ),
+%%
+%% %% RedirectUri = https://my.provider/logout?id_token_hint=IDToken&client_id=ClientId&post_logout_redirect_uri=https%3A%2F%2Fmy.server%2Freturn
+%% '''
+%% @end
+%% @since 3.0.0
+-spec initiate_url(Token, ClientContext, Opts) ->
+ {ok, uri_string:uri_string()} | {error, error()}
+when
+ Token :: IdToken | oidcc_token:t() | undefined,
+ IdToken :: binary(),
+ ClientContext :: oidcc_client_context:t(),
+ Opts :: initiate_url_opts().
+initiate_url(#oidcc_token{id = #oidcc_token_id{token = IdToken}}, ClientContext, Opts) ->
+ initiate_url(IdToken, ClientContext, Opts);
+initiate_url(IdToken, ClientContext, Opts) ->
+ #oidcc_client_context{
+ provider_configuration = Configuration,
+ client_id = ClientId
+ } = ClientContext,
+ #oidcc_provider_configuration{end_session_endpoint = EndSessionEndpoint} =
+ Configuration,
+
+ QueryParams0 = [
+ {"id_token_hint", IdToken},
+ {"logout_hint", maps:get(logout_hint, Opts, undefined)},
+ {"client_id", ClientId},
+ {"post_logout_redirect_uri", maps:get(post_logout_redirect_uri, Opts, undefined)},
+ {"state", maps:get(state, Opts, undefined)},
+ {"ui_locales", maps:get(ui_locales, Opts, undefined)}
+ | maps:get(extra_query_params, Opts, [])
+ ],
+ QueryParams1 = lists:filter(
+ fun
+ ({_Name, undefined}) -> false;
+ ({_Name, _Value}) -> true
+ end,
+ QueryParams0
+ ),
+
+ case EndSessionEndpoint of
+ undefined ->
+ {error, end_session_endpoint_not_supported};
+ Uri0 ->
+ UriMap0 = uri_string:parse(Uri0),
+ QueryString0 = maps:get(query, UriMap0, <<"">>),
+ QueryParams = uri_string:dissect_query(QueryString0) ++ QueryParams1,
+ QueryString = uri_string:compose_query(QueryParams),
+ UriMap = maps:put(query, QueryString, UriMap0),
+ Uri = uri_string:recompose(UriMap),
+ {ok, Uri}
+ end.
diff --git a/src/oidcc_provider_configuration.erl b/src/oidcc_provider_configuration.erl
index e0a55e8..87d5f6a 100644
--- a/src/oidcc_provider_configuration.erl
+++ b/src/oidcc_provider_configuration.erl
@@ -96,12 +96,14 @@
introspection_endpoint_auth_signing_alg_values_supported ::
[binary()] | undefined,
code_challenge_methods_supported :: [binary()] | undefined,
+ end_session_endpoint :: uri_string:uri_string() | undefined,
extra_fields :: #{binary() => term()}
}.
%% Record containing OpenID and OAuth 2.0 Configuration
%%
-%% See [https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata] and
-%% [https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1]
+%% See [https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata],
+%% [https://datatracker.ietf.org/doc/html/draft-jones-oauth-discovery-01#section-4.1] and
+%% [https://openid.net/specs/openid-connect-rpinitiated-1_0.html#OPMetadata]
%%
%% All unrecognized fields are stored in `extra_fields'.
@@ -292,7 +294,8 @@ decode_configuration(Configuration) ->
IntrospectionEndpointAuthMethodsSupported,
introspection_endpoint_auth_signing_alg_values_supported :=
IntrospectionEndpointAuthSigningAlgValuesSupported,
- code_challenge_methods_supported := CodeChallengeMethodsSupported
+ code_challenge_methods_supported := CodeChallengeMethodsSupported,
+ end_session_endpoint := EndSessionEndpoint
},
ExtraFields
}} ?=
@@ -376,7 +379,9 @@ decode_configuration(Configuration) ->
{optional, introspection_endpoint_auth_signing_alg_values_supported, undefined,
fun parse_token_signing_alg_values_no_none/2},
{optional, code_challenge_methods_supported, undefined,
- fun oidcc_decode_util:parse_setting_binary_list/2}
+ fun oidcc_decode_util:parse_setting_binary_list/2},
+ {optional, end_session_endpoint, undefined,
+ fun oidcc_decode_util:parse_setting_uri_https/2}
],
#{}
),
@@ -441,6 +446,7 @@ decode_configuration(Configuration) ->
IntrospectionEndpointAuthSigningAlgValuesSupported,
code_challenge_methods_supported =
CodeChallengeMethodsSupported,
+ end_session_endpoint = EndSessionEndpoint,
extra_fields = ExtraFields
}}
end.
diff --git a/test/oidcc/logout_test.exs b/test/oidcc/logout_test.exs
new file mode 100644
index 0000000..c14988a
--- /dev/null
+++ b/test/oidcc/logout_test.exs
@@ -0,0 +1,31 @@
+defmodule Oidcc.LogoutTest do
+ use ExUnit.Case, async: true
+
+ alias Oidcc.Logout
+
+ doctest Logout
+
+ describe inspect(&Logout.create_redirect_url/3) do
+ test "works" do
+ pid =
+ start_supervised!(
+ {Oidcc.ProviderConfiguration.Worker,
+ %{issuer: "https://erlef-test-w4a8z2.zitadel.cloud"}}
+ )
+
+ {:ok, client_context} =
+ Oidcc.ClientContext.from_configuration_worker(
+ pid,
+ "client_id",
+ "client_secret"
+ )
+
+ assert {:ok, _redirect_uri} =
+ Logout.initiate_url(
+ "token",
+ client_context,
+ %{post_logout_redirect_uri: "https://my.server/return"}
+ )
+ end
+ end
+end
diff --git a/test/oidcc_SUITE.erl b/test/oidcc_SUITE.erl
index 5593882..7fa5923 100644
--- a/test/oidcc_SUITE.erl
+++ b/test/oidcc_SUITE.erl
@@ -3,6 +3,7 @@
-export([all/0]).
-export([create_redirect_url/1]).
-export([end_per_suite/1]).
+-export([initiate_logout_url/1]).
-export([init_per_suite/1]).
-export([introspect_token/1]).
-export([refresh_token/1]).
@@ -21,6 +22,7 @@ all() ->
retrieve_token,
retrieve_userinfo,
refresh_token,
+ initiate_logout_url,
introspect_token,
retrieve_jwt_profile_token,
retrieve_client_credentials_token
@@ -203,3 +205,23 @@ retrieve_client_credentials_token(_Config) ->
),
ok.
+
+initiate_logout_url(_Config) ->
+ {ok, ZitadelConfigurationPid} =
+ oidcc_provider_configuration_worker:start_link(#{
+ issuer => <<"https://erlef-test-w4a8z2.zitadel.cloud">>
+ }),
+
+ {ok, Uri} = oidcc:initiate_logout_url(
+ #oidcc_token{id = #oidcc_token_id{token = <<"id_token">>}},
+ ZitadelConfigurationPid,
+ <<"client_id">>,
+ <<"client_secret">>,
+ #{}
+ ),
+ ?assertEqual(
+ <<"https://erlef-test-w4a8z2.zitadel.cloud/oidc/v1/end_session?id_token_hint=id_token&client_id=client_id">>,
+ iolist_to_binary(Uri)
+ ),
+
+ ok.
diff --git a/test/oidcc_client_registration_test.erl b/test/oidcc_client_registration_test.erl
index a54b402..c397ebd 100644
--- a/test/oidcc_client_registration_test.erl
+++ b/test/oidcc_client_registration_test.erl
@@ -53,7 +53,6 @@ register_test() ->
?assertMatch(
#{
<<"application_type">> := <<"web">>,
- <<"extra_fields">> := #{},
<<"redirect_uris">> := [RedirectUri],
<<"require_auth_time">> := false,
<<"token_endpoint_auth_method">> := <<"client_secret_basic">>
diff --git a/test/oidcc_logout_test.erl b/test/oidcc_logout_test.erl
new file mode 100644
index 0000000..2dffc6d
--- /dev/null
+++ b/test/oidcc_logout_test.erl
@@ -0,0 +1,61 @@
+-module(oidcc_logout_test).
+
+-include_lib("eunit/include/eunit.hrl").
+-include_lib("oidcc/include/oidcc_provider_configuration.hrl").
+-include_lib("oidcc/include/oidcc_token.hrl").
+
+initiate_url_test() ->
+ PrivDir = code:priv_dir(oidcc),
+
+ {ok, ValidConfigString} = file:read_file(PrivDir ++ "/test/fixtures/example-metadata.json"),
+ {ok, Configuration} = oidcc_provider_configuration:decode_configuration(
+ jose:decode(ValidConfigString)
+ ),
+ Jwks = jose_jwk:from_pem_file(PrivDir ++ "/test/fixtures/jwk.pem"),
+
+ NormalConfiguration = Configuration#oidcc_provider_configuration{
+ end_session_endpoint = <<"https://example.provider/logout">>
+ },
+ NormalClientContext = oidcc_client_context:from_manual(
+ NormalConfiguration, Jwks, <<"client_id">>, <<"client_secret">>
+ ),
+
+ QueryParamConfiguration = Configuration#oidcc_provider_configuration{
+ end_session_endpoint = <<"https://example.provider/logout?query=param">>
+ },
+ QueryParamClientContext = oidcc_client_context:from_manual(
+ QueryParamConfiguration, Jwks, <<"client_id">>, <<"client_secret">>
+ ),
+
+ NoEndSessionEndpointConfiguration = Configuration#oidcc_provider_configuration{
+ end_session_endpoint = undefined
+ },
+ NoEndSessionEndpointClientContext = oidcc_client_context:from_manual(
+ NoEndSessionEndpointConfiguration, Jwks, <<"client_id">>, <<"client_secret">>
+ ),
+
+ ?assertMatch(
+ {error, end_session_endpoint_not_supported},
+ oidcc_logout:initiate_url(<<"id_token">>, NoEndSessionEndpointClientContext, #{})
+ ),
+
+ {ok, NormalUri0} = oidcc_logout:initiate_url(
+ #oidcc_token{id = #oidcc_token_id{token = <<"id_token">>}}, NormalClientContext, #{}
+ ),
+ ?assertEqual(
+ <<"https://example.provider/logout?id_token_hint=id_token&client_id=client_id">>,
+ iolist_to_binary(NormalUri0)
+ ),
+
+ {ok, NormalUri1} = oidcc_logout:initiate_url(undefined, NormalClientContext, #{}),
+ ?assertEqual(
+ <<"https://example.provider/logout?client_id=client_id">>, iolist_to_binary(NormalUri1)
+ ),
+
+ {ok, QueryParamsUri} = oidcc_logout:initiate_url(undefined, QueryParamClientContext, #{}),
+ ?assertEqual(
+ <<"https://example.provider/logout?query=param&client_id=client_id">>,
+ iolist_to_binary(QueryParamsUri)
+ ),
+
+ ok.
diff --git a/test/oidcc_test.exs b/test/oidcc_test.exs
index e569841..ca92c7d 100644
--- a/test/oidcc_test.exs
+++ b/test/oidcc_test.exs
@@ -183,4 +183,21 @@ defmodule OidccTest do
)
end
end
+
+ describe inspect(&Oidcc.initiate_logout_url/5) do
+ test "works" do
+ pid =
+ start_supervised!(
+ {ProviderConfiguration.Worker, %{issuer: "https://erlef-test-w4a8z2.zitadel.cloud"}}
+ )
+
+ assert {:ok, _redirect_uri} =
+ Oidcc.initiate_logout_url(
+ "id_token",
+ pid,
+ "client_id",
+ "client_secret"
+ )
+ end
+ end
end