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

+%% +%% + +%% @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