/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

/***************************************************************************
 * Copyright (C) 2017-2026 ZmartZone Holding BV
 * All rights reserved.
 *
 * DISCLAIMER OF WARRANTIES:
 *
 * THE SOFTWARE PROVIDED HEREUNDER IS PROVIDED ON AN "AS IS" BASIS, WITHOUT
 * ANY WARRANTIES OR REPRESENTATIONS EXPRESS, IMPLIED OR STATUTORY; INCLUDING,
 * WITHOUT LIMITATION, WARRANTIES OF QUALITY, PERFORMANCE, NONINFRINGEMENT,
 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  NOR ARE THERE ANY
 * WARRANTIES CREATED BY A COURSE OR DEALING, COURSE OF PERFORMANCE OR TRADE
 * USAGE.  FURTHERMORE, THERE ARE NO WARRANTIES THAT THE SOFTWARE WILL MEET
 * YOUR NEEDS OR BE FREE FROM ERRORS, OR THAT THE OPERATION OF THE SOFTWARE
 * WILL BE UNINTERRUPTED.  IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @Author: Hans Zandbelt - hans.zandbelt@openidc.com
 */

#include "cfg/dir.h"
#include "handle/handle.h"
#include "metrics.h"
#include "mod_auth_openidc.h"
#include "proto/proto.h"
#include "util/util.h"
#include "util/util_cfg.h"

#define OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT_ENVVAR "OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT"

#define OIDC_TOKEN_REVOCATION_AUD_ENV_VAR "OIDC_TOKEN_REVOCATION_AUD"

/*
 * revoke a single token of the indicated type at the RFC 7009 revocation endpoint,
 * doing nothing when the session does not hold such a token
 */
static void oidc_logout_revoke_one_token(request_rec *r, oidc_cfg_t *c, const oidc_provider_t *provider,
					 apr_table_t *params, const char *basic_auth, const char *bearer_auth,
					 const char *token_type_hint, const char *token) {
	char *response = NULL;

	if (token == NULL)
		return;

	apr_table_setn(params, OIDC_PROTO_TOKEN_TYPE_HINT, token_type_hint);
	apr_table_setn(params, OIDC_PROTO_TOKEN, token);

	if (oidc_http_post_form(r, oidc_cfg_provider_revocation_endpoint_url_get(provider), params, basic_auth,
				bearer_auth, NULL, oidc_cfg_provider_ssl_validate_server_get(provider), &response, NULL,
				NULL, oidc_cfg_http_timeout_long_get(c), oidc_cfg_outgoing_proxy_get(c),
				oidc_cfg_dir_pass_cookies_get(r),
				oidc_cfg_provider_token_endpoint_tls_client_cert_get(provider),
				oidc_cfg_provider_token_endpoint_tls_client_key_get(provider),
				oidc_cfg_provider_token_endpoint_tls_client_key_pwd_get(provider)) == FALSE) {
		OIDC_METRICS_COUNTER_INC(r, c, OM_PROVIDER_REVOCATION_ERROR);
		oidc_warn(r, "revoking %s failed", token_type_hint);
	}

	apr_table_unset(params, OIDC_PROTO_TOKEN_TYPE_HINT);
	apr_table_unset(params, OIDC_PROTO_TOKEN);
}

/*
 * revoke refresh token and access token stored in the session if the
 * OP has an RFC 7009 compliant token revocation endpoint
 */
static void oidc_logout_revoke_tokens(request_rec *r, oidc_cfg_t *c, const oidc_session_t *session) {

	char *basic_auth = NULL;
	char *bearer_auth = NULL;
	apr_table_t *params = NULL;
	oidc_provider_t *provider = NULL;

	oidc_debug(r, "enter");

	if (oidc_get_provider_from_session(r, c, session, &provider) == FALSE)
		goto out;

	if (apr_table_get(r->subprocess_env, OIDC_DONT_REVOKE_TOKENS_BEFORE_LOGOUT_ENVVAR) != NULL)
		goto out;

	oidc_debug(r, "revocation_endpoint=%s",
		   oidc_cfg_provider_revocation_endpoint_url_get(provider)
		       ? oidc_cfg_provider_revocation_endpoint_url_get(provider)
		       : "(null)");

	if ((oidc_cfg_provider_revocation_endpoint_url_get(provider) == NULL) ||
	    (_oidc_strcmp(oidc_cfg_provider_revocation_endpoint_url_get(provider), "") == 0))
		goto out;

	params = apr_table_make(r->pool, 4);

	// add the token endpoint authentication credentials to the revocation endpoint call...
	if (oidc_proto_token_endpoint_auth(
		r, c, oidc_cfg_provider_token_endpoint_auth_get(provider),
		oidc_cfg_provider_token_endpoint_auth_alg_get(provider), oidc_cfg_provider_client_id_get(provider),
		oidc_cfg_provider_client_secret_get(provider), oidc_cfg_provider_client_keys_get(provider),
		oidc_proto_profile_revocation_endpoint_auth_aud(
		    provider, apr_table_get(r->subprocess_env, OIDC_TOKEN_REVOCATION_AUD_ENV_VAR)),
		params, NULL, &basic_auth, &bearer_auth) == FALSE)
		goto out;

	oidc_logout_revoke_one_token(r, c, provider, params, basic_auth, bearer_auth, OIDC_PROTO_REFRESH_TOKEN,
				     oidc_session_get_refresh_token(r, session));
	oidc_logout_revoke_one_token(r, c, provider, params, basic_auth, bearer_auth, OIDC_PROTO_ACCESS_TOKEN,
				     oidc_session_get_access_token(r, session));

out:

	oidc_debug(r, "leave");
}

static apr_byte_t oidc_logout_cleanup_by_sid(request_rec *r, char *sid, oidc_cfg_t *cfg,
					     const oidc_provider_t *provider, apr_byte_t revoke_tokens) {

	char *uuid = NULL;
	oidc_session_t session;
	apr_byte_t loaded = FALSE;

	oidc_debug(r, "enter (sid=%s,iss=%s)", sid, oidc_cfg_provider_issuer_get(provider));

	/*
	 * NB: when a logout token carries "sub" rather than a true "sid" (either because the OP does not use
	 * "sid", or per OpenID Connect Back-Channel Logout 1.0 section 2.6), the session is located via the
	 * "sub"-based index. When multiple hosts share the *same* cache backend and OIDCCryptoPassphrase that
	 * index is shared, so a "sub"-based logout will also invalidate the corresponding session on those hosts
	 * (non-memory caching is encrypted by default and shm caching uses per-host segments, which limits this).
	 */

	sid = oidc_response_make_sid_iss_unique(r, sid, oidc_cfg_provider_issuer_get(provider));
	oidc_cache_get_sid(r, sid, &uuid);
	if (uuid == NULL) {
		// this may happen when we are the caller
		oidc_warn(
		    r,
		    "could not (or no longer) find a session based on sid/sub provided in logout token / parameter: %s",
		    sid);
		r->user = "";
		return TRUE;
	}

	// load the session so we can (optionally) revoke its tokens and clean up all of its cache index entries
	if (oidc_cfg_session_type_get(cfg) != OIDC_SESSION_TYPE_CLIENT_COOKIE)
		loaded = (oidc_session_load_cache_by_uuid(r, cfg, uuid, &session) != FALSE) &&
			 (oidc_session_extract(r, &session) != FALSE);

	// revoke tokens if we can get a handle on those
	if ((loaded == TRUE) && (revoke_tokens == TRUE))
		oidc_logout_revoke_tokens(r, cfg, &session);

	// clear the session and both its sid/sub cache index entries (it may have been located via either)
	oidc_cache_set_sid(r, sid, NULL, 0);
	if (loaded == TRUE) {
		if (session.sid != NULL)
			oidc_cache_set_sid(r, session.sid, NULL, 0);
		if (session.sub != NULL)
			oidc_cache_set_sid(r, session.sub, NULL, 0);
		oidc_session_free(r, &session);
	}
	oidc_cache_set_session(r, uuid, NULL, 0);

	r->user = "";
	return FALSE;
}

static apr_uint32_t oidc_logout_transparent_pixel[17] = {
    0x474e5089, 0x0a1a0a0d, 0x0d000000, 0x52444849, 0x01000000, 0x01000000, 0x00000408, 0x0c1cb500, 0x00000002,
    0x4144490b, 0x639c7854, 0x0000cffa, 0x02010702, 0x71311c9a, 0x00000000, 0x444e4549, 0x826042ae};

static apr_byte_t oidc_logout_is_front_channel(const char *logout_param_value) {
	return ((logout_param_value != NULL) &&
		((_oidc_strcmp(logout_param_value, OIDC_GET_STYLE_LOGOUT_PARAM_VALUE) == 0) ||
		 (_oidc_strcmp(logout_param_value, OIDC_IMG_STYLE_LOGOUT_PARAM_VALUE) == 0)));
}

static apr_byte_t oidc_logout_is_back_channel(const char *logout_param_value) {
	return ((logout_param_value != NULL) &&
		(_oidc_strcmp(logout_param_value, OIDC_BACKCHANNEL_STYLE_LOGOUT_PARAM_VALUE) == 0));
}

/*
 * resolve the provider for a front-channel logout based on the sid/iss
 * parameters from the request as specified in "OpenID Connect Front-Channel
 * Logout 1.0 - draft 05" at https://openid.net/specs/openid-connect-frontchannel-1_0.html;
 * *sid is set when the OP supplied a sid parameter, regardless of whether a
 * provider could be resolved
 */
static oidc_provider_t *oidc_logout_request_front_channel_provider(request_rec *r, oidc_cfg_t *c, char **sid) {

	char *iss = NULL;
	oidc_provider_t *provider = NULL;

	if (oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_SID, sid) == FALSE)
		return NULL;

	if (oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_ISS, &iss) != FALSE)
		return oidc_get_provider_for_issuer(r, c, iss, FALSE);

	/*
	 * Microsoft Entra ID / Azure AD seems to be such a non spec compliant provider.
	 * In this case try our luck with the static config if possible.
	 */
	oidc_debug(r, "OP did not provide an iss as parameter");
	if (oidc_provider_static_config(r, c, &provider) == FALSE)
		return NULL;
	return provider;
}

/*
 * handle the front-channel branch of a local logout: try to clear the
 * session based on sid/iss (when no session was provided), emit the
 * recommended cache-control headers and return the iframe body or
 * transparent pixel response
 *
 * NB: this branch is reached without any proof that the caller owns the session it names: a
 * front-channel logout carries no signature (unlike a back-channel logout token) and, by the
 * nature of the third-party iframe it is called from, no session cookie either. It therefore
 * does *not* revoke the session's tokens at the OP, exactly as the back-channel handler does
 * not; that is a side effect outside of this server which the user cannot undo by logging in
 * again. Clearing the local session is what front-channel logout is for and is kept.
 */
static int oidc_logout_request_front_channel(request_rec *r, oidc_cfg_t *c, const char *url,
					     apr_byte_t no_session_provided) {

	if (no_session_provided) {
		char *sid = NULL;
		const oidc_provider_t *provider = oidc_logout_request_front_channel_provider(r, c, &sid);
		if (provider != NULL)
			oidc_logout_cleanup_by_sid(r, sid, c, provider, FALSE);
		else if (sid != NULL)
			oidc_info(r, "No provider for front channel logout found");
	}

	/* set recommended cache control headers */
	oidc_http_set_no_cache_headers(r);
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_P3P, "CAO PSA OUR");
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_EXPIRES, "0");
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_X_FRAME_OPTIONS, oidc_cfg_logout_x_frame_options_get(c));

	/* see if this is PF-PA style logout in which case we return a transparent pixel */
	const char *accept = oidc_http_hdr_in_accept_get(r);
	if ((_oidc_strcmp(url, OIDC_IMG_STYLE_LOGOUT_PARAM_VALUE) == 0) ||
	    (accept && _oidc_strstr(accept, OIDC_HTTP_CONTENT_TYPE_IMAGE_PNG))) {
		return oidc_util_http_content_prep(r, (const char *)&oidc_logout_transparent_pixel,
						   sizeof(oidc_logout_transparent_pixel),
						   OIDC_HTTP_CONTENT_TYPE_IMAGE_PNG);
	}

	/* standard HTTP based logout: should be called in an iframe from the OP */
	return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Logged Out", NULL, NULL,
					   "<p>Logged Out</p>");
}

/*
 * handle a local logout
 */
int oidc_logout_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session, const char *url,
			apr_byte_t revoke_tokens) {

	apr_byte_t no_session_provided = TRUE;

	oidc_debug(r, "enter (url=%s)", url);

	/*
	 * if there's no remote_user then there's no (stored) session to kill; note that revoking the
	 * tokens is tied to having one, i.e. to the caller having proven that the session is theirs
	 */
	if (session->remote_user != NULL) {
		no_session_provided = FALSE;
		if (revoke_tokens)
			oidc_logout_revoke_tokens(r, c, session);
	}

	/*
	 * remove session state (cq. cache entry and cookie)
	 * always clear the session cookie because the cookie may be not sent (but still in the browser)
	 * due to SameSite policies
	 */
	oidc_session_kill(r, session);

	/* see if this is the OP calling us */
	if (oidc_logout_is_front_channel(url))
		return oidc_logout_request_front_channel(r, c, url, no_session_provided);

	oidc_http_set_no_cache_headers(r);

	/* see if we don't need to go somewhere special after killing the session locally */
	if (url == NULL)
		return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_HTML, "Logged Out", NULL, NULL,
						   "<p>Logged Out</p>");

	/* send the user to the specified where-to-go-after-logout URL */
	oidc_http_hdr_out_location_set(r, url);

	return HTTP_MOVED_TEMPORARILY;
}

/*
 * handle a backchannel logout
 */
#define OIDC_EVENTS_BLOGOUT_KEY "http://schemas.openid.net/event/backchannel-logout"

static int oidc_logout_backchannel_read_token(request_rec *r, const char **logout_token) {
	apr_table_t *params = apr_table_make(r->pool, 8);
	if (oidc_util_read_post_params(r, params, FALSE, NULL) == FALSE) {
		oidc_error(r, "could not read POST-ed parameters to the logout endpoint");
		return HTTP_BAD_REQUEST;
	}
	*logout_token = apr_table_get(params, OIDC_PROTO_LOGOUT_TOKEN);
	if (*logout_token == NULL) {
		oidc_error(r, "backchannel lggout endpoint was called but could not find a parameter named \"%s\"",
			   OIDC_PROTO_LOGOUT_TOKEN);
		return HTTP_BAD_REQUEST;
	}
	return OK;
}

static int oidc_logout_backchannel_parse_jwt(request_rec *r, oidc_cfg_t *cfg, const char *logout_token,
					     oidc_jwt_t **jwt) {
	oidc_jose_error_t err;
	oidc_jwk_t *jwk = NULL;
	char *alg = NULL;

	/*
	 * a back-channel logout token is unsolicited, so the issuer - and thus the specific provider - is not
	 * known until the token has been parsed; when the token is symmetrically encrypted it will have been
	 * encrypted with a key derived from the configured provider's client secret, so peek the (unencrypted)
	 * JOSE header to size that key correctly and add it to the decryption key set alongside the RP's private
	 * keys (cjose selects a compatible key for the actual decryption)
	 */
	oidc_proto_jwt_header_peek(r, logout_token, &alg, NULL, NULL);
	oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(oidc_cfg_provider_get(cfg)),
				       oidc_alg2keysize(alg), OIDC_JOSE_ALG_SHA256, TRUE, &jwk);

	if (oidc_jwt_parse(r->pool, logout_token, jwt,
			   oidc_util_key_symmetric_merge(r->pool, oidc_cfg_private_keys_get(cfg), jwk), FALSE,
			   &err) == FALSE) {
		oidc_error(r, "oidc_jwt_parse failed: %s", oidc_jose_e2s(r->pool, err));
		oidc_jwk_destroy(jwk);
		return HTTP_BAD_REQUEST;
	}

	oidc_jwk_destroy(jwk);

	if (((*jwt)->header.alg == NULL) || (_oidc_strcmp((*jwt)->header.alg, "none") == 0)) {
		oidc_error(r, "logout token is not signed");
		return HTTP_BAD_REQUEST;
	}
	return OK;
}

static int oidc_logout_backchannel_get_provider(request_rec *r, oidc_cfg_t *cfg, oidc_jwt_t *jwt,
						oidc_provider_t **provider) {
	*provider = oidc_get_provider_for_issuer(r, cfg, jwt->payload.iss, FALSE);
	if (*provider == NULL) {
		oidc_error(r, "no provider found for issuer: %s", jwt->payload.iss);
		return HTTP_BAD_REQUEST;
	}
	if ((oidc_cfg_provider_id_token_signed_response_alg_get(*provider) != NULL) &&
	    (_oidc_strcmp(oidc_cfg_provider_id_token_signed_response_alg_get(*provider), jwt->header.alg) != 0)) {
		oidc_error(r, "logout token is signed using wrong algorithm: %s != %s", jwt->header.alg,
			   oidc_cfg_provider_id_token_signed_response_alg_get(*provider));
		return HTTP_BAD_REQUEST;
	}
	return OK;
}

static int oidc_logout_backchannel_verify_jwt(request_rec *r, oidc_cfg_t *cfg, oidc_jwt_t *jwt,
					      const oidc_provider_t *provider, const oidc_jwk_t *jwk) {
	if (oidc_proto_jwt_verify(
		r, cfg, jwt, oidc_cfg_provider_jwks_uri_get(provider),
		oidc_cfg_provider_ssl_validate_server_get(provider),
		oidc_util_key_symmetric_merge(r->pool, oidc_cfg_provider_verify_public_keys_get(provider), jwk),
		oidc_cfg_provider_id_token_signed_response_alg_get(provider)) == FALSE) {
		oidc_error(r, "id_token signature could not be validated, aborting");
		return HTTP_BAD_REQUEST;
	}
	/* NB: "iat" is REQUIRED in a logout token (OpenID Connect Back-Channel Logout 1.0, 2.4),
	 * and it is what bounds replay: without it a captured token stays valid indefinitely once
	 * the jti cache entry expires. "exp" is not required by that spec, so it stays optional. */
	if (oidc_proto_jwt_validate(
		r, jwt, oidc_cfg_provider_validate_issuer_get(provider) ? oidc_cfg_provider_issuer_get(provider) : NULL,
		FALSE, TRUE, oidc_cfg_provider_idtoken_iat_slack_get(provider)) == FALSE)
		return HTTP_BAD_REQUEST;
	/* verify the "aud" and "azp" values */
	if (oidc_proto_idtoken_validate_aud_and_azp(r, cfg, provider, &jwt->payload) == FALSE)
		return HTTP_BAD_REQUEST;
	return OK;
}

static int oidc_logout_backchannel_check_jti_replay(request_rec *r, const oidc_provider_t *provider,
						    const oidc_jwt_t *jwt) {
	char *jti = NULL;
	char *replay = NULL;
	apr_time_t jti_cache_duration;

	oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_JTI, &jti, NULL);

	/* "jti" is REQUIRED in a logout token (OpenID Connect Back-Channel Logout 1.0, 2.4) and is
	 * what makes replay detectable at all; a token without one used to skip the check entirely
	 * and then write a cache entry under a NULL key that no lookup ever reads */
	if (jti == NULL) {
		oidc_error(r, "logout token does not contain the required \"%s\" claim", OIDC_CLAIM_JTI);
		return HTTP_BAD_REQUEST;
	}

	oidc_cache_get_jti(r, jti, &replay);
	if (replay != NULL) {
		oidc_error(r,
			   "the \"%s\" value (%s) passed in logout token was found in the cache already; "
			   "possible replay attack!?",
			   OIDC_CLAIM_JTI, jti);
		return HTTP_BAD_REQUEST;
	}

	/* jti cache duration is the configured replay prevention window for token issuance plus 10 seconds for safety
	 */
	jti_cache_duration = apr_time_from_sec(oidc_cfg_provider_idtoken_iat_slack_get(provider) * 2 + 10);
	/* store it in the cache for the calculated duration */
	oidc_cache_set_jti(r, jti, jti, apr_time_now() + jti_cache_duration);
	return OK;
}

static int oidc_logout_backchannel_validate_claims(request_rec *r, const oidc_provider_t *provider, oidc_jwt_t *jwt,
						   char **sid) {
	char *nonce = NULL;
	const oidc_json_t *events = NULL;
	const oidc_json_t *blogout = NULL;
	int rc = OK;

	events = oidc_json_object_get(jwt->payload.value.json, OIDC_CLAIM_EVENTS);
	if (events == NULL) {
		oidc_error(r, "\"%s\" claim could not be found in logout token", OIDC_CLAIM_EVENTS);
		return HTTP_BAD_REQUEST;
	}
	blogout = oidc_json_object_get(events, OIDC_EVENTS_BLOGOUT_KEY);
	if (!oidc_json_is_object(blogout)) {
		oidc_error(r, "\"%s\" object could not be found in \"%s\" claim", OIDC_EVENTS_BLOGOUT_KEY,
			   OIDC_CLAIM_EVENTS);
		return HTTP_BAD_REQUEST;
	}

	oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_NONCE, &nonce, NULL);
	if (nonce != NULL) {
		oidc_error(r, "rejecting logout request/token since it contains a \"%s\" claim", OIDC_CLAIM_NONCE);
		return HTTP_BAD_REQUEST;
	}

	rc = oidc_logout_backchannel_check_jti_replay(r, provider, jwt);
	if (rc != OK)
		return rc;

	/*
	 * a logout token may carry "sub" even when the id_token returned a "sid" (OpenID Connect Back-Channel
	 * Logout 1.0 section 2.6); when the OP supports back-channel logout the session is additionally indexed
	 * by "sub" at creation time (see oidc_response_save_in_session), so cleanup below resolves it either way
	 */
	oidc_json_object_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_SID, sid, NULL);
	if (*sid == NULL)
		*sid = jwt->payload.sub;
	if (*sid == NULL) {
		oidc_error(r, "no \"sub\" and no \"sid\" claim found in logout token");
		return HTTP_BAD_REQUEST;
	}
	return OK;
}

static int oidc_logout_backchannel(request_rec *r, oidc_cfg_t *cfg) {
	int rc = HTTP_BAD_REQUEST;
	const char *logout_token = NULL;
	oidc_jwt_t *jwt = NULL;
	oidc_jwk_t *jwk = NULL;
	oidc_provider_t *provider = NULL;
	char *sid = NULL;

	oidc_debug(r, "enter");

	rc = oidc_logout_backchannel_read_token(r, &logout_token);
	if (rc != OK)
		goto out;

	rc = oidc_logout_backchannel_parse_jwt(r, cfg, logout_token, &jwt);
	if (rc != OK)
		goto out;

	rc = oidc_logout_backchannel_get_provider(r, cfg, jwt, &provider);
	if (rc != OK)
		goto out;

	if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), 0, NULL, TRUE, &jwk) ==
	    FALSE) {
		rc = HTTP_BAD_REQUEST;
		goto out;
	}

	rc = oidc_logout_backchannel_verify_jwt(r, cfg, jwt, provider, jwk);
	if (rc != OK)
		goto out;

	rc = oidc_logout_backchannel_validate_claims(r, provider, jwt, &sid);
	if (rc != OK)
		goto out;

	// a backchannel logout comes from the provider, so no need to revoke the tokens
	oidc_logout_cleanup_by_sid(r, sid, cfg, provider, FALSE);

	rc = OK;

out:

	if (rc == OK) {
		OIDC_METRICS_COUNTER_INC(r, cfg, OM_LOGOUT_BACKCHANNEL);
	} else {
		OIDC_METRICS_COUNTER_INC(r, cfg, OM_LOGOUT_BACKCHANNEL_ERROR);
	}

	if (jwk != NULL)
		oidc_jwk_destroy(jwk);
	if (jwt != NULL)
		oidc_jwt_destroy(jwt);

	oidc_http_set_no_cache_headers(r);

	return rc;
}

#define OIDC_REFRESH_TOKENS_BEFORE_LOGOUT_ENVVAR "OIDC_REFRESH_TOKENS_BEFORE_LOGOUT"

/*
 * append a query string suffix to url, picking '?' or '&' based on whether
 * url already contains a query string
 */
static char *oidc_logout_url_append(request_rec *r, const char *url, const char *suffix) {
	const char *sep = (strchr(url, OIDC_CHAR_QUERY) != NULL) ? OIDC_STR_AMP : OIDC_STR_QUERY;
	return apr_pstrcat(r->pool, url, sep, suffix, NULL);
}

/*
 * apply the default and validate the post-logout redirect URL; on failure
 * sets *rv to the HTTP status the caller should return
 */
static apr_byte_t oidc_logout_validate_url(request_rec *r, const oidc_cfg_t *c, char **url, int *rv) {

	char *error_str = NULL;
	char *error_description = NULL;

	if ((*url == NULL) || (_oidc_strcmp(*url, "") == 0)) {
		*url = apr_pstrdup(r->pool, oidc_util_url_abs(r, c, oidc_cfg_default_slo_url_get(c)));
		return TRUE;
	}

	/* do input validation on the logout parameter value */
	if (oidc_validate_redirect_url(r, c, *url, OIDC_REDIRECT_URL_SAME_HOST, &error_str, &error_description) ==
	    FALSE) {
		*rv = oidc_util_html_send_error(r, error_str, error_description, HTTP_BAD_REQUEST);
		return FALSE;
	}
	return TRUE;
}

/*
 * build the OP end_session_endpoint URL, populating id_token_hint
 * (optionally refreshed), post_logout_redirect_uri and any configured
 * extra logout request parameters
 */
static char *oidc_logout_build_op_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session,
					  const oidc_provider_t *provider, const char *post_logout_url) {

	char *id_token_hint = NULL;
	char *s_logout_request = NULL;

	if (apr_table_get(r->subprocess_env, OIDC_REFRESH_TOKENS_BEFORE_LOGOUT_ENVVAR) != NULL) {
		if (oidc_refresh_token_grant(r, c, session, provider, NULL, NULL, &id_token_hint) == FALSE)
			oidc_warn(r, "id_token_hint could not be refreshed before logout");
	} else {
		id_token_hint = apr_pstrdup(r->pool, oidc_session_get_idtoken(r, session));
	}

	s_logout_request = apr_pstrdup(r->pool, oidc_cfg_provider_end_session_endpoint_get(provider));
	if (id_token_hint != NULL)
		s_logout_request = oidc_logout_url_append(
		    r, s_logout_request,
		    apr_psprintf(r->pool, OIDC_PROTO_ID_TOKEN_HINT "=%s", oidc_http_url_encode(r, id_token_hint)));

	if (post_logout_url != NULL)
		s_logout_request = oidc_logout_url_append(
		    r, s_logout_request,
		    apr_psprintf(r->pool, "post_logout_redirect_uri=%s", oidc_http_url_encode(r, post_logout_url)));

	if (oidc_cfg_provider_logout_request_params_get(provider) != NULL)
		s_logout_request =
		    oidc_logout_url_append(r, s_logout_request, oidc_cfg_provider_logout_request_params_get(provider));

	return s_logout_request;
}

/*
 * perform (single) logout
 */
int oidc_logout(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) {

	/* pickup the command or URL where the user wants to go after logout */
	char *url = NULL;
	oidc_provider_t *provider = NULL;
	int rv = OK;

	oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_LOGOUT, &url);

	oidc_debug(r, "enter (url=%s)", url);

	if (oidc_logout_is_front_channel(url))
		return oidc_logout_request(r, c, session, url, TRUE);
	if (oidc_logout_is_back_channel(url))
		return oidc_logout_backchannel(r, c);

	if (oidc_logout_validate_url(r, c, &url, &rv) == FALSE)
		return rv;

	if (oidc_get_provider_from_session(r, c, session, &provider) == FALSE)
		oidc_warn(r, "oidc_get_provider_from_session failed");

	if ((provider != NULL) && (oidc_cfg_provider_end_session_endpoint_get(provider) != NULL))
		url = oidc_logout_build_op_request(r, c, session, provider, url);

	return oidc_logout_request(r, c, session, url, TRUE);
}
