src/oidcc_logout.erl

-module(oidcc_logout).

-feature(maybe_expr, enable).

-include("internal/doc.hrl").
?MODULEDOC("Logout from the OpenID Provider.").
?MODULEDOC(#{since => <<"3.0.0">>}).

-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]).

?DOC(#{since => <<"3.0.0">>}).
-type error() :: end_session_endpoint_not_supported.

?DOC("""
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(#{since => <<"3.0.0">>}).
-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()
}.

?DOC("""
Initiate URI for Relaying Party initiated Logout.

See https://openid.net/specs/openid-connect-rpinitiated-1_0.html#RPLogout.

For a high level interface using `m:oidcc_provider_configuration_worker`
see `oidcc:initiate_logout_url/4`.

## Examples

```erlang
{ok, ClientContext} = oidcc_client_context:from_configuration_worker(
  provider_name,
  <<"client_id">>,
  unauthenticated
),

%% 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
```
""").
?DOC(#{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.