/*
 * 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/cache.h"
#include "cfg/cfg_int.h"
#include "cfg/dir.h"
#include "cfg/oauth.h"
#include "cfg/parse.h"
#include "cfg/provider.h"
#include "jose.h"
#include "metrics.h"
#include "proto/proto.h"
#include "session.h"
#include "util/util.h"
#include "util/util_cfg.h"

#if OPENSSL_API_COMPAT < 0x10100000L
#include <openssl/err.h>
#include <openssl/ssl.h>
#endif

const char *oidc_cfg_string_list_add(apr_pool_t *pool, apr_array_header_t **list, const char *arg) {
	if (*list == NULL)
		*list = apr_array_make(pool, 1, sizeof(const char *));
	APR_ARRAY_PUSH(*list, const char *) = arg;
	return NULL;
}

#define OIDC_DEFAULT_ACTION_ON_USERINFO_ERROR OIDC_ON_ERROR_502
OIDC_CFG_MEMBER_FUNC_TYPE_GET(action_on_userinfo_error, oidc_on_error_action_t, OIDC_DEFAULT_ACTION_ON_USERINFO_ERROR)

#define OIDC_CFG_MEMBER_FUNCS_HTTP_TIMEOUT(member, def_val)                                                            \
	const char *oidc_cmd_##member##_set(cmd_parms *cmd, void *ptr, const char *arg1, const char *arg2,             \
					    const char *arg3) {                                                        \
		oidc_cfg_t *cfg =                                                                                      \
		    (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);              \
		const char *rv = oidc_cfg_parse_http_timeout(cmd->pool, arg1, arg2, arg3, &cfg->member);               \
		return OIDC_CONFIG_DIR_RV(cmd, rv);                                                                    \
	}                                                                                                              \
                                                                                                                       \
	oidc_http_timeout_t *oidc_cfg_##member##_get(oidc_cfg_t *cfg) {                                                \
		if (cfg->member.request_timeout == OIDC_CONFIG_POS_INT_UNSET)                                          \
			/* NB: we are modifying in-config but post_config/merge has finished by now  */                \
			cfg->member.request_timeout = def_val;                                                         \
		return &cfg->member;                                                                                   \
	}

/* request timeout in seconds for HTTP calls that may take a long time */
#define OIDC_DEFAULT_HTTP_REQUEST_TIMEOUT_LONG 30
/* connect timeout in seconds for HTTP calls that may take a long time */
#define OIDC_DEFAULT_HTTP_CONNECT_TIMEOUT_LONG 10
/* nr of retries for HTTP calls that may take a long time */
#define OIDC_DEFAULT_HTTP_RETRIES_LONG 1
/* retry interval in milliseconds for HTTP calls that may take a long time */
#define OIDC_DEFAULT_HTTP_RETRY_INTERVAL_LONG 500
/* timeouts in seconds for HTTP calls that should take a short time (registry/discovery related) */
#define OIDC_DEFAULT_HTTP_REQUEST_TIMEOUT_SHORT 5
/* connect timeout in seconds for HTTP calls that may take a long time */
#define OIDC_DEFAULT_HTTP_CONNECT_TIMEOUT_SHORT 2
/* nr of retries for HTTP calls that should take a short time */
#define OIDC_DEFAULT_HTTP_RETRIES_SHORT 1
/* retry interval in milliseconds for HTTP calls that should take a short time */
#define OIDC_DEFAULT_HTTP_RETRY_INTERVAL_SHORT 300

OIDC_CFG_MEMBER_FUNCS_HTTP_TIMEOUT(http_timeout_long, OIDC_DEFAULT_HTTP_REQUEST_TIMEOUT_LONG)
OIDC_CFG_MEMBER_FUNCS_HTTP_TIMEOUT(http_timeout_short, OIDC_DEFAULT_HTTP_REQUEST_TIMEOUT_SHORT)

const char *oidc_cmd_crypto_passphrase_set(cmd_parms *cmd, void *struct_ptr, const char *arg1, const char *arg2) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = NULL;
	char *v = NULL;
	if (arg1) {
		rv = oidc_cfg_parse_passphrase(cmd->pool, arg1, &v);
		if (rv == NULL)
			cfg->crypto_passphrase.secret1 = v;
	}
	if ((rv == NULL) && (arg2 != NULL)) {
		rv = oidc_cfg_parse_passphrase(cmd->pool, arg2, &v);
		if (rv == NULL)
			cfg->crypto_passphrase.secret2 = v;
	}
	return rv;
}

const oidc_crypto_passphrase_t *oidc_cfg_crypto_passphrase_get(const oidc_cfg_t *cfg) {
	return &cfg->crypto_passphrase;
}

const char *oidc_cfg_crypto_passphrase_secret1_get(const oidc_cfg_t *cfg) {
	return cfg->crypto_passphrase.secret1;
}

void oidc_cfg_crypto_passphrase_secret1_set(oidc_cfg_t *cfg, const char *secret) {
	cfg->crypto_passphrase.secret1 = secret;
}

const char *oidc_cfg_crypto_passphrase_secret2_get(const oidc_cfg_t *cfg) {
	return cfg->crypto_passphrase.secret2;
}

/* Derive finalized passphrases once, before requests use them for JWE operations. */
apr_byte_t oidc_crypto_passphrase_derive_keys(oidc_crypto_passphrase_t *cp) {
	/* an empty (non-NULL) secret is treated as "not configured", matching
	 * oidc_util_key_symmetric_create()'s existing behavior for a client_secret */
	if ((cp->secret1 != NULL) && (_oidc_strlen(cp->secret1) > 0) && (cp->derived_key1_set == FALSE)) {
		if (oidc_util_key_derive_passphrase_key(cp->secret1, cp->derived_key1,
							OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN) == FALSE)
			return FALSE;
		cp->derived_key1_set = TRUE;
	}

	if ((cp->secret2 != NULL) && (_oidc_strlen(cp->secret2) > 0) && (cp->derived_key2_set == FALSE)) {
		if (oidc_util_key_derive_passphrase_key(cp->secret2, cp->derived_key2,
							OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN) == FALSE)
			return FALSE;
		cp->derived_key2_set = TRUE;
	}

	return TRUE;
}

/*
 * thin wrapper around oidc_crypto_passphrase_derive_keys() for the passphrase embedded in a
 * server config; see that function for details
 */
apr_byte_t oidc_cfg_crypto_passphrase_derive_keys(oidc_cfg_t *cfg) {
	return oidc_crypto_passphrase_derive_keys(&cfg->crypto_passphrase);
}

/*
 * Memoize PBKDF2 output by secret across vhosts. The pool must outlive kdf_cache; a NULL cache
 * disables memoization.
 */
apr_byte_t oidc_crypto_passphrase_derive_keys_cached(apr_pool_t *pool, apr_hash_t *kdf_cache,
						     oidc_crypto_passphrase_t *cp) {
	const unsigned char *cached = NULL;

	if (kdf_cache == NULL)
		return oidc_crypto_passphrase_derive_keys(cp);

	if ((cp->secret1 != NULL) && (_oidc_strlen(cp->secret1) > 0) && (cp->derived_key1_set == FALSE)) {
		cached = apr_hash_get(kdf_cache, cp->secret1, APR_HASH_KEY_STRING);
		if (cached != NULL) {
			_oidc_memcpy(cp->derived_key1, cached, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
		} else {
			if (oidc_util_key_derive_passphrase_key(cp->secret1, cp->derived_key1,
								OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN) == FALSE)
				return FALSE;
			apr_hash_set(kdf_cache, apr_pstrdup(pool, cp->secret1), APR_HASH_KEY_STRING,
				     apr_pmemdup(pool, cp->derived_key1, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN));
		}
		cp->derived_key1_set = TRUE;
	}

	if ((cp->secret2 != NULL) && (_oidc_strlen(cp->secret2) > 0) && (cp->derived_key2_set == FALSE)) {
		cached = apr_hash_get(kdf_cache, cp->secret2, APR_HASH_KEY_STRING);
		if (cached != NULL) {
			_oidc_memcpy(cp->derived_key2, cached, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
		} else {
			if (oidc_util_key_derive_passphrase_key(cp->secret2, cp->derived_key2,
								OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN) == FALSE)
				return FALSE;
			apr_hash_set(kdf_cache, apr_pstrdup(pool, cp->secret2), APR_HASH_KEY_STRING,
				     apr_pmemdup(pool, cp->derived_key2, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN));
		}
		cp->derived_key2_set = TRUE;
	}

	return TRUE;
}

/*
 * thin wrapper around oidc_crypto_passphrase_derive_keys_cached() for the passphrase embedded
 * in a server config; see that function for details
 */
apr_byte_t oidc_cfg_crypto_passphrase_derive_keys_cached(apr_pool_t *pool, apr_hash_t *kdf_cache, oidc_cfg_t *cfg) {
	return oidc_crypto_passphrase_derive_keys_cached(pool, kdf_cache, &cfg->crypto_passphrase);
}

const char *oidc_cmd_outgoing_proxy_set(cmd_parms *cmd, void *ptr, const char *arg1, const char *arg2,
					const char *arg3) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = NULL;
	if (arg1)
		cfg->outgoing_proxy.host_port = apr_pstrdup(cmd->pool, arg1);
	if (arg2)
		cfg->outgoing_proxy.username_password = apr_pstrdup(cmd->pool, arg2);
	if (arg3) {
		rv = oidc_cfg_parse_is_valid_option(cmd->pool, arg3, oidc_http_proxy_auth_options());
		if (rv == NULL)
			cfg->outgoing_proxy.auth_type = oidc_http_proxy_s2auth(arg3);
	}
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

const oidc_http_outgoing_proxy_t *oidc_cfg_outgoing_proxy_get(const oidc_cfg_t *cfg) {
	return &cfg->outgoing_proxy;
}

static const char *oidc_cfg_valid_cookie_domain(apr_pool_t *pool, const char *arg) {
	size_t limit = _oidc_strlen(arg);
	char d;
	for (size_t sz = 0; sz < limit; sz++) {
		d = arg[sz];
		if ((d < '0' || d > '9') && (d < 'a' || d > 'z') && (d < 'A' || d > 'Z') && d != '.' && d != '-') {
			return (apr_psprintf(pool, "invalid character '%c' in cookie domain value: %s", d, arg));
		}
	}
	return NULL;
}

OIDC_CFG_MEMBER_FUNCS_TYPE(cookie_domain, const char *, oidc_cfg_valid_cookie_domain(cmd->pool, arg))

#define OIDC_SESSION_TYPE_SERVER_CACHE_STR "server-cache"
#define OIDC_SESSION_TYPE_CLIENT_COOKIE_STR "client-cookie"
#define OIDC_SESSION_TYPE_PERSISTENT "persistent"
#define OIDC_SESSION_TYPE_STORE_ID_TOKEN "store_id_token"
#define OIDC_SESSION_TYPE_SEPARATOR ":"

const char *oidc_cmd_session_type_set(cmd_parms *cmd, void *ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);

	static const char *options[] = {
	    OIDC_SESSION_TYPE_SERVER_CACHE_STR,
	    OIDC_SESSION_TYPE_SERVER_CACHE_STR OIDC_SESSION_TYPE_SEPARATOR OIDC_SESSION_TYPE_PERSISTENT,
	    OIDC_SESSION_TYPE_CLIENT_COOKIE_STR,
	    OIDC_SESSION_TYPE_CLIENT_COOKIE_STR OIDC_SESSION_TYPE_SEPARATOR OIDC_SESSION_TYPE_PERSISTENT,
	    OIDC_SESSION_TYPE_CLIENT_COOKIE_STR OIDC_SESSION_TYPE_SEPARATOR OIDC_SESSION_TYPE_STORE_ID_TOKEN,
	    OIDC_SESSION_TYPE_CLIENT_COOKIE_STR OIDC_SESSION_TYPE_SEPARATOR OIDC_SESSION_TYPE_PERSISTENT
		OIDC_SESSION_TYPE_SEPARATOR OIDC_SESSION_TYPE_STORE_ID_TOKEN,
	    NULL};

	const char *rv = oidc_cfg_parse_is_valid_option(cmd->pool, arg, options);
	if (rv != NULL)
		return OIDC_CONFIG_DIR_RV(cmd, rv);

	const char *s = apr_pstrdup(cmd->pool, arg);
	char *p = _oidc_strstr(s, OIDC_SESSION_TYPE_SEPARATOR);

	if (p) {
		*p = '\0';
		p++;
	}

	if (_oidc_strcmp(s, OIDC_SESSION_TYPE_SERVER_CACHE_STR) == 0) {
		cfg->session_type = OIDC_SESSION_TYPE_SERVER_CACHE;
	} else if (_oidc_strcmp(s, OIDC_SESSION_TYPE_CLIENT_COOKIE_STR) == 0) {
		cfg->session_type = OIDC_SESSION_TYPE_CLIENT_COOKIE;
		cfg->store_id_token = 0;
	}

	/* the combination was validated against options[] above, so each remaining
	 * separator-delimited modifier can be applied on its own */
	while (p) {
		char *next = _oidc_strstr(p, OIDC_SESSION_TYPE_SEPARATOR);
		if (next) {
			*next = '\0';
			next++;
		}
		if (_oidc_strcmp(p, OIDC_SESSION_TYPE_PERSISTENT) == 0)
			cfg->persistent_session_cookie = 1;
		else if (_oidc_strcmp(p, OIDC_SESSION_TYPE_STORE_ID_TOKEN) == 0)
			// only for client-cookie
			cfg->store_id_token = 1;
		p = next;
	}

	return NULL;
}

#define OIDC_DEFAULT_SESSION_TYPE OIDC_SESSION_TYPE_SERVER_CACHE
OIDC_CFG_MEMBER_FUNC_TYPE_GET(session_type, int, OIDC_DEFAULT_SESSION_TYPE)

#define OIDC_DEFAULT_PERSISTENT_SESSION_COOKIE 0
OIDC_CFG_MEMBER_FUNC_TYPE_GET(persistent_session_cookie, int, OIDC_DEFAULT_PERSISTENT_SESSION_COOKIE)

#define OIDC_DEFAULT_STORE_ID_TOKEN 1
OIDC_CFG_MEMBER_FUNC_TYPE_GET(store_id_token, int, OIDC_DEFAULT_STORE_ID_TOKEN)

static const char *oidc_valid_endpoint_auth_method_impl(apr_pool_t *pool, const char *arg, apr_byte_t has_private_key,
							apr_byte_t allow_mtls) {
	/* Build per call because private-key validation changes the candidate list. */
	const char *options[9];
	int i = 0;
	options[i++] = OIDC_ENDPOINT_AUTH_CLIENT_SECRET_POST;
	options[i++] = OIDC_ENDPOINT_AUTH_CLIENT_SECRET_BASIC;
	options[i++] = OIDC_ENDPOINT_AUTH_CLIENT_SECRET_JWT;
	options[i++] = OIDC_ENDPOINT_AUTH_NONE;
	options[i++] = OIDC_ENDPOINT_AUTH_BEARER_ACCESS_TOKEN;
	if (has_private_key)
		options[i++] = OIDC_ENDPOINT_AUTH_PRIVATE_KEY_JWT;
	if (allow_mtls) {
		options[i++] = OIDC_ENDPOINT_AUTH_TLS_CLIENT_AUTH;
		options[i++] = OIDC_ENDPOINT_AUTH_SELF_SIGNED_TLS_CLIENT_AUTH;
	}
	options[i] = NULL;
	return oidc_cfg_parse_is_valid_option(pool, arg, options);
}

static const char *oidc_valid_private_key_jwt_alg(apr_pool_t *pool, const char *arg) {
	static const char *options[] = {"RS256", "RS384", "RS512", "PS256", "PS384", "PS512",
#if (OIDC_JOSE_EC_SUPPORT)
					"ES256", "ES384", "ES512",
#endif

					NULL};
	return oidc_cfg_parse_is_valid_option(pool, arg, options);
}

#define OIDC_ENDPOINT_AUTH_METHOD_SEPARATOR ":"

static void oidc_cfg_endpoint_auth_parse(apr_pool_t *pool, const char *arg1, char **method, char **suffix) {
	const char *arg = apr_pstrdup(pool, arg1);
	char *sep = _oidc_strstr(arg, OIDC_ENDPOINT_AUTH_METHOD_SEPARATOR);
	if (sep) {
		*sep = '\0';
		*suffix = apr_pstrdup(pool, ++sep);
	}
	*method = apr_pstrdup(pool, arg);
}

static const char *oidc_cfg_valid_endpoint_auth_method_alg(apr_pool_t *pool, const char *arg1, apr_byte_t allow_mtls) {
	const char *rv = NULL;
	char *method = NULL;
	char *alg = NULL;
	oidc_cfg_endpoint_auth_parse(pool, arg1, &method, &alg);
	if ((_oidc_strcmp(method, OIDC_ENDPOINT_AUTH_PRIVATE_KEY_JWT) == 0) && (alg != NULL)) {
		rv = oidc_valid_private_key_jwt_alg(pool, alg);
		if (rv != NULL)
			return rv;
	}
	return oidc_valid_endpoint_auth_method_impl(pool, method, TRUE, allow_mtls);
}

static const char *oidc_cfg_valid_endpoint_auth_method_with_private_key(apr_pool_t *pool, const char *arg1) {
	return oidc_cfg_valid_endpoint_auth_method_alg(pool, arg1, TRUE);
}

static const char *oidc_cfg_valid_endpoint_auth_method_no_private_key(apr_pool_t *pool, const char *arg) {
	return oidc_valid_endpoint_auth_method_impl(pool, arg, FALSE, TRUE);
}

static const char *oidc_cfg_valid_endpoint_auth_method_no_mtls_with_private_key(apr_pool_t *pool, const char *arg1) {
	return oidc_cfg_valid_endpoint_auth_method_alg(pool, arg1, FALSE);
}

static const char *oidc_cfg_valid_endpoint_auth_method_no_mtls_no_private_key(apr_pool_t *pool, const char *arg) {
	return oidc_valid_endpoint_auth_method_impl(pool, arg, FALSE, FALSE);
}

const char *oidc_cfg_endpoint_auth_set(apr_pool_t *pool, const oidc_cfg_t *cfg, const char *arg1, char **auth,
				       char **alg) {
	const char *rv = oidc_cfg_get_valid_endpoint_auth_function(cfg, TRUE)(pool, arg1);
	if (rv == NULL)
		oidc_cfg_endpoint_auth_parse(pool, arg1, auth, alg);
	return rv;
}

/*
 * check if the endpoint authentication method is one of the RFC 8705 mutual-TLS methods
 */
apr_byte_t oidc_cfg_endpoint_auth_is_mtls(const char *method) {
	return (_oidc_strcmp(method, OIDC_ENDPOINT_AUTH_TLS_CLIENT_AUTH) == 0) ||
	       (_oidc_strcmp(method, OIDC_ENDPOINT_AUTH_SELF_SIGNED_TLS_CLIENT_AUTH) == 0);
}

/* Select endpoint-auth validation based on private-key availability and whether mTLS is allowed. */
oidc_valid_function_t oidc_cfg_get_valid_endpoint_auth_function(const oidc_cfg_t *cfg, apr_byte_t allow_mtls) {
	if (allow_mtls == FALSE)
		return (cfg->private_keys != NULL) ? &oidc_cfg_valid_endpoint_auth_method_no_mtls_with_private_key
						   : &oidc_cfg_valid_endpoint_auth_method_no_mtls_no_private_key;
	return (cfg->private_keys != NULL) ? &oidc_cfg_valid_endpoint_auth_method_with_private_key
					   : &oidc_cfg_valid_endpoint_auth_method_no_private_key;
}

#define OIDC_SESSION_INACTIVITY_TIMEOUT_MIN 10
#define OIDC_SESSION_INACTIVITY_TIMEOUT_MAX (3600 * 24 * 365)
#define OIDC_DEFAULT_SESSION_INACTIVITY_TIMEOUT 300

OIDC_CFG_MEMBER_FUNCS_INT(session_inactivity_timeout, OIDC_SESSION_INACTIVITY_TIMEOUT_MIN,
			  OIDC_SESSION_INACTIVITY_TIMEOUT_MAX, OIDC_DEFAULT_SESSION_INACTIVITY_TIMEOUT)

const char *oidc_cmd_public_keys_set(cmd_parms *cmd, void *ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_cfg_parse_public_key_files(cmd->pool, arg, &cfg->public_keys);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(public_keys, const apr_array_header_t *)

/*
 * add a private key from an RSA/EC private key file to our list of JWKs with private keys
 */
const char *oidc_cmd_private_keys_set(cmd_parms *cmd, void *ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_cfg_parse_private_key_files(cmd->pool, arg, &cfg->private_keys);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(private_keys, const apr_array_header_t *)

const char *oidc_cmd_remote_user_claim_set(cmd_parms *cmd, void *ptr, const char *v1, const char *v2, const char *v3) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_parse_remote_user_claim(cmd->pool, v1, v2, v3, &cfg->remote_user_claim);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

const oidc_remote_user_claim_t *oidc_cfg_remote_user_claim_get(const oidc_cfg_t *cfg) {
	return &cfg->remote_user_claim;
}

#define OIDC_DEFAULT_CLAIM_REMOTE_USER "sub@"

const char *oidc_cfg_remote_user_claim_name_get(const oidc_cfg_t *cfg) {
	return cfg->remote_user_claim.claim_name != NULL ? cfg->remote_user_claim.claim_name
							 : OIDC_DEFAULT_CLAIM_REMOTE_USER;
}

#ifdef USE_LIBJQ

const char *oidc_cmd_filter_claims_expr_set(cmd_parms *cmd, void *m, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_util_apr_expr_parse(cmd, arg, &cfg->filter_claims_expr, OIDC_APR_EXPR_RESULT_STRING);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#endif

OIDC_CFG_MEMBER_FUNC_GET(filter_claims_expr, oidc_apr_expr_t *)

/*
 * define which data will be returned from the info hook
 */
const char *oidc_cmd_info_hook_data_set(cmd_parms *cmd, void *m, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	static const char *options[] = {
	    OIDC_HOOK_INFO_TIMESTAMP,		OIDC_HOOK_INFO_ACCES_TOKEN, OIDC_HOOK_INFO_ACCES_TOKEN_EXP,
	    OIDC_HOOK_INFO_ID_TOKEN_HINT,	OIDC_HOOK_INFO_ID_TOKEN,    OIDC_HOOK_INFO_USER_INFO,
	    OIDC_HOOK_INFO_REFRESH_TOKEN,	OIDC_HOOK_INFO_SESSION_EXP, OIDC_HOOK_INFO_SESSION_TIMEOUT,
	    OIDC_HOOK_INFO_SESSION_REMOTE_USER, OIDC_HOOK_INFO_SESSION,	    NULL};
	const char *rv = oidc_cfg_parse_is_valid_option(cmd->pool, arg, options);
	if (rv != NULL)
		return OIDC_CONFIG_DIR_RV(cmd, rv);
	if (cfg->info_hook_data == NULL)
		cfg->info_hook_data = apr_hash_make(cmd->pool);
	apr_hash_set(cfg->info_hook_data, arg, APR_HASH_KEY_STRING, arg);
	return NULL;
}

OIDC_CFG_MEMBER_FUNC_GET(info_hook_data, apr_hash_t *)

const char *oidc_cmd_metrics_hook_data_set(cmd_parms *cmd, void *m, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = NULL;
	char *valid_names = NULL;
	if (oidc_metrics_is_valid_classname(cmd->pool, arg, &valid_names) == TRUE) {
		if (cfg->metrics_hook_data == NULL)
			cfg->metrics_hook_data = apr_hash_make(cmd->pool);
		apr_hash_set(cfg->metrics_hook_data, arg, APR_HASH_KEY_STRING, arg);
	} else {
		rv = apr_psprintf(cmd->pool, "undefined metric class name: \"%s\", must be one of [%s]", arg,
				  valid_names);
	}
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(metrics_hook_data, apr_hash_t *)

#define OIDC_TRACE_PARENT_OFF_STR "off"
#define OIDC_TRACE_PARENT_PROPAGATE_STR "propagate"
#define OIDC_TRACE_PARENT_GENERATE_STR "generate"

const char *oidc_cmd_trace_parent_set(cmd_parms *cmd, void *struct_ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	static const oidc_cfg_option_t options[] = {{OIDC_TRACE_PARENT_OFF, OIDC_TRACE_PARENT_OFF_STR},
						    {OIDC_TRACE_PARENT_PROPAGATE, OIDC_TRACE_PARENT_PROPAGATE_STR},
						    {OIDC_TRACE_PARENT_GENERATE, OIDC_TRACE_PARENT_GENERATE_STR}};
	const char *rv =
	    oidc_cfg_parse_option(cmd->pool, options, OIDC_CFG_OPTIONS_SIZE(options), arg, &cfg->trace_parent);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#define OIDC_DEFAULT_TRACE_PARENT OIDC_TRACE_PARENT_OFF
OIDC_CFG_MEMBER_FUNC_TYPE_GET(trace_parent, oidc_trace_parent_t, OIDC_DEFAULT_TRACE_PARENT)

#define OIDC_DEFAULT_DPOP_API_ENABLED 0
OIDC_CFG_MEMBER_FUNC_TYPE_GET(dpop_api_enabled, int, OIDC_DEFAULT_DPOP_API_ENABLED)

const char *oidc_cmd_claim_prefix_set(cmd_parms *cmd, void *struct_ptr, const char *args) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	char *w = ap_getword_conf(cmd->pool, &args);
	if (*w == '\0' || *args != 0)
		cfg->claim_prefix = "";
	else
		cfg->claim_prefix = w;
	return NULL;
}

#define OIDC_DEFAULT_CLAIM_PREFIX "OIDC_CLAIM_"

const char *oidc_cfg_claim_prefix_get(const oidc_cfg_t *cfg) {
	return (cfg->claim_prefix != NULL) ? cfg->claim_prefix : OIDC_DEFAULT_CLAIM_PREFIX;
}

#define OIDC_MAX_NUMBER_OF_STATE_COOKIES_MIN 0
#define OIDC_MAX_NUMBER_OF_STATE_COOKIES_MAX 255

const char *oidc_cmd_max_number_of_state_cookies_set(cmd_parms *cmd, void *struct_ptr, const char *arg1,
						     const char *arg2) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv =
	    oidc_cfg_parse_int_min_max(cmd->pool, arg1, &cfg->max_number_of_state_cookies,
				       OIDC_MAX_NUMBER_OF_STATE_COOKIES_MIN, OIDC_MAX_NUMBER_OF_STATE_COOKIES_MAX);
	if ((rv == NULL) && (arg2 != NULL))
		rv = oidc_cfg_parse_boolean(cmd->pool, arg2, &cfg->delete_oldest_state_cookies);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#define OIDC_DEFAULT_MAX_NUMBER_OF_STATE_COOKIES 7
OIDC_CFG_MEMBER_FUNC_TYPE_GET(max_number_of_state_cookies, int, OIDC_DEFAULT_MAX_NUMBER_OF_STATE_COOKIES)

#define OIDC_DEFAULT_DELETE_OLDEST_STATE_COOKIES 0
OIDC_CFG_MEMBER_FUNC_TYPE_GET(delete_oldest_state_cookies, int, OIDC_DEFAULT_DELETE_OLDEST_STATE_COOKIES)

const char *oidc_cmd_x_forwarded_headers_set(cmd_parms *cmd, void *m, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	static const oidc_cfg_option_t options[] = {{OIDC_HDR_NONE, "none"},
						    {OIDC_HDR_X_FORWARDED_HOST, OIDC_HTTP_HDR_X_FORWARDED_HOST},
						    {OIDC_HDR_X_FORWARDED_PORT, OIDC_HTTP_HDR_X_FORWARDED_PORT},
						    {OIDC_HDR_X_FORWARDED_PROTO, OIDC_HTTP_HDR_X_FORWARDED_PROTO},
						    {OIDC_HDR_FORWARDED, OIDC_HTTP_HDR_FORWARDED}};
	int v = OIDC_CONFIG_POS_INT_UNSET;
	const char *rv = oidc_cfg_parse_option(cmd->pool, options, OIDC_CFG_OPTIONS_SIZE(options), arg, &v);
	if ((rv == NULL) && (v != OIDC_CONFIG_POS_INT_UNSET)) {
		// NB: cannot use |= with UNSET/-1 !
		if (cfg->x_forwarded_headers == OIDC_CONFIG_POS_INT_UNSET)
			cfg->x_forwarded_headers = OIDC_HDR_NONE;
		cfg->x_forwarded_headers |= v;
	}
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#define OIDC_DEFAULT_X_FORWARDED_HEADERS OIDC_HDR_NONE
OIDC_CFG_MEMBER_FUNC_TYPE_GET(x_forwarded_headers, oidc_hdr_x_forwarded_t, OIDC_DEFAULT_X_FORWARDED_HEADERS)

#define OIDC_CHECK_X_FORWARDED_HDR_LOG_DISABLE "OIDC_CHECK_X_FORWARDED_HDR_LOG_DISABLE"

static void oidc_check_x_forwarded_hdr(request_rec *r, const oidc_hdr_x_forwarded_t x_forwarded_headers,
				       const oidc_hdr_x_forwarded_t hdr_type, const char *hdr_str,
				       const char *(hdr_func)(const request_rec *r)) {
	apr_byte_t suppress = oidc_util_spaced_string_contains(
	    r->pool, apr_table_get(r->subprocess_env, OIDC_CHECK_X_FORWARDED_HDR_LOG_DISABLE), hdr_str);
	if (hdr_func(r)) {
		if (!(x_forwarded_headers & hdr_type) && !suppress)
			oidc_warn(r, "header %s received but %s not configured for it", hdr_str, OIDCXForwardedHeaders);
	} else {
		if ((x_forwarded_headers & hdr_type) && !suppress)
			oidc_warn(r, "%s configured for header %s but not found in request", OIDCXForwardedHeaders,
				  hdr_str);
	}
}

void oidc_cfg_x_forwarded_headers_check(request_rec *r, oidc_hdr_x_forwarded_t x_forwarded_headers) {
	oidc_check_x_forwarded_hdr(r, x_forwarded_headers, OIDC_HDR_X_FORWARDED_HOST, OIDC_HTTP_HDR_X_FORWARDED_HOST,
				   oidc_http_hdr_in_x_forwarded_host_get);
	oidc_check_x_forwarded_hdr(r, x_forwarded_headers, OIDC_HDR_X_FORWARDED_PORT, OIDC_HTTP_HDR_X_FORWARDED_PORT,
				   oidc_http_hdr_in_x_forwarded_port_get);
	oidc_check_x_forwarded_hdr(r, x_forwarded_headers, OIDC_HDR_X_FORWARDED_PROTO, OIDC_HTTP_HDR_X_FORWARDED_PROTO,
				   oidc_http_hdr_in_x_forwarded_proto_get);
	oidc_check_x_forwarded_hdr(r, x_forwarded_headers, OIDC_HDR_FORWARDED, OIDC_HTTP_HDR_FORWARDED,
				   oidc_http_hdr_in_forwarded_get);
}

#define OIDC_STATE_INPUT_HEADERS_AS_NONE "none"
#define OIDC_STATE_INPUT_HEADERS_AS_USER_AGENT "user-agent"
#define OIDC_STATE_INPUT_HEADERS_AS_X_FORWARDED_FOR "x-forwarded-for"
#define OIDC_STATE_INPUT_HEADERS_AS_BOTH "both"

/*
 * define which header we use for calculating the fingerprint of the state during authentication
 */
const char *oidc_cmd_state_input_headers_set(cmd_parms *cmd, void *m, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	static const oidc_cfg_option_t options[] = {
	    {OIDC_STATE_INPUT_HEADERS_NONE, OIDC_STATE_INPUT_HEADERS_AS_NONE},
	    {OIDC_STATE_INPUT_HEADERS_USER_AGENT, OIDC_STATE_INPUT_HEADERS_AS_USER_AGENT},
	    {OIDC_STATE_INPUT_HEADERS_X_FORWARDED_FOR, OIDC_STATE_INPUT_HEADERS_AS_X_FORWARDED_FOR},
	    {OIDC_STATE_INPUT_HEADERS_USER_AGENT | OIDC_STATE_INPUT_HEADERS_X_FORWARDED_FOR,
	     OIDC_STATE_INPUT_HEADERS_AS_BOTH}};
	const char *rv =
	    oidc_cfg_parse_option(cmd->pool, options, OIDC_CFG_OPTIONS_SIZE(options), arg, &cfg->state_input_headers);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#define OIDC_DEFAULT_STATE_INPUT_HEADERS OIDC_STATE_INPUT_HEADERS_USER_AGENT
OIDC_CFG_MEMBER_FUNC_TYPE_GET(state_input_headers, oidc_state_input_hdrs_t, OIDC_DEFAULT_STATE_INPUT_HEADERS)

const char *oidc_cmd_post_preserve_templates_set(cmd_parms *cmd, void *m, const char *arg1, const char *arg2) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = NULL;
	if (arg1 != NULL)
		rv = oidc_cfg_parse_filename(cmd->pool, arg1, &cfg->post_preserve_template);
	if ((rv == NULL) && (arg2 != NULL))
		rv = oidc_cfg_parse_filename(cmd->pool, arg2, &cfg->post_restore_template);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(post_preserve_template, const char *)
OIDC_CFG_MEMBER_FUNC_GET(post_restore_template, const char *)

const char *oidc_cmd_ca_bundle_path_set(cmd_parms *cmd, void *ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_cfg_parse_filename(cmd->pool, arg, &cfg->ca_bundle_path);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(ca_bundle_path, const char *)

const char *oidc_cmd_metadata_dir_set(cmd_parms *cmd, void *ptr, const char *arg) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	const char *rv = oidc_cfg_parse_dirname(cmd->pool, arg, &cfg->metadata_dir);
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

OIDC_CFG_MEMBER_FUNC_GET(metadata_dir, const char *)

#define OIDC_DEFAULT_COOKIE_HTTPONLY 1
OIDC_CFG_MEMBER_FUNCS_BOOL(cookie_http_only, OIDC_DEFAULT_COOKIE_HTTPONLY)

#define OIDC_SAMESITE_COOKIE_OFF_STR "Off"
#define OIDC_SAMESITE_COOKIE_ON_STR "On"
#define OIDC_SAMESITE_COOKIE_DISABLED_STR "Disabled"
#define OIDC_SAMESITE_COOKIE_NONE_STR "None"
#define OIDC_SAMESITE_COOKIE_LAX_STR "Lax"
#define OIDC_SAMESITE_COOKIE_STRICT_STR "Strict"

/*
 * define which header we use for calculating the fingerprint of the state during authentication
 */
const char *oidc_cmd_cookie_same_site_session_set(cmd_parms *cmd, void *m, const char *arg1, const char *arg2,
						  const char *arg3) {
	oidc_cfg_t *cfg = (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);
	// NB: On is made equal to Lax here and Off is equal to None (backwards compatibility)
	static const oidc_cfg_option_t options[] = {{OIDC_SAMESITE_COOKIE_NONE, OIDC_SAMESITE_COOKIE_OFF_STR},
						    {OIDC_SAMESITE_COOKIE_LAX, OIDC_SAMESITE_COOKIE_ON_STR},
						    {OIDC_SAMESITE_COOKIE_DISABLED, OIDC_SAMESITE_COOKIE_DISABLED_STR},
						    {OIDC_SAMESITE_COOKIE_NONE, OIDC_SAMESITE_COOKIE_NONE_STR},
						    {OIDC_SAMESITE_COOKIE_LAX, OIDC_SAMESITE_COOKIE_LAX_STR},
						    {OIDC_SAMESITE_COOKIE_STRICT, OIDC_SAMESITE_COOKIE_STRICT_STR}};
	const char *rv = oidc_cfg_parse_option_ignore_case(cmd->pool, options, OIDC_CFG_OPTIONS_SIZE(options), arg1,
							   &cfg->cookie_same_site_session);
	if ((rv == NULL) && (arg2 != NULL)) {
		static const oidc_cfg_option_t state_options[] = {
		    {OIDC_SAMESITE_COOKIE_DISABLED, OIDC_SAMESITE_COOKIE_DISABLED_STR},
		    {OIDC_SAMESITE_COOKIE_NONE, OIDC_SAMESITE_COOKIE_NONE_STR},
		    {OIDC_SAMESITE_COOKIE_LAX, OIDC_SAMESITE_COOKIE_LAX_STR}};
		rv = oidc_cfg_parse_option_ignore_case(cmd->pool, state_options, OIDC_CFG_OPTIONS_SIZE(state_options),
						       arg2, &cfg->cookie_same_site_state);
	}
	if ((rv == NULL) && (arg3 != NULL)) {
		static const oidc_cfg_option_t csrf_options[] = {
		    {OIDC_SAMESITE_COOKIE_DISABLED, OIDC_SAMESITE_COOKIE_DISABLED_STR},
		    {OIDC_SAMESITE_COOKIE_NONE, OIDC_SAMESITE_COOKIE_NONE_STR},
		    {OIDC_SAMESITE_COOKIE_LAX, OIDC_SAMESITE_COOKIE_LAX_STR},
		    {OIDC_SAMESITE_COOKIE_STRICT, OIDC_SAMESITE_COOKIE_STRICT_STR}};
		rv = oidc_cfg_parse_option_ignore_case(cmd->pool, csrf_options, OIDC_CFG_OPTIONS_SIZE(csrf_options),
						       arg3, &cfg->cookie_same_site_discovery_csrf);
	}
	return OIDC_CONFIG_DIR_RV(cmd, rv);
}

#define OIDC_DEFAULT_COOKIE_SAME_SITE OIDC_SAMESITE_COOKIE_LAX
OIDC_CFG_MEMBER_FUNC_TYPE_GET(cookie_same_site_session, oidc_samesite_cookie_t, OIDC_DEFAULT_COOKIE_SAME_SITE)

#define OIDC_DEFAULT_COOKIE_SAME_SITE_STATE oidc_cfg_cookie_same_site_session_get(cfg)
OIDC_CFG_MEMBER_FUNC_TYPE_GET(cookie_same_site_state, oidc_samesite_cookie_t, OIDC_DEFAULT_COOKIE_SAME_SITE_STATE)

#define OIDC_DEFAULT_COOKIE_SAME_SITE_CSRF_DISCOVERY oidc_cfg_cookie_same_site_session_get(cfg)
OIDC_CFG_MEMBER_FUNC_TYPE_GET(cookie_same_site_discovery_csrf, oidc_samesite_cookie_t,
			      OIDC_DEFAULT_COOKIE_SAME_SITE_CSRF_DISCOVERY)

#define OIDC_DEFAULT_SESSION_FALLBACK_TO_COOKIE 0
OIDC_CFG_MEMBER_FUNCS_BOOL(session_cache_fallback_to_cookie, OIDC_DEFAULT_SESSION_FALLBACK_TO_COOKIE)

/* masking secrets in the debug log is the default; turning it off is a deliberate, logged choice */
#define OIDC_DEFAULT_DEBUG_MASK_SECRETS 1
OIDC_CFG_MEMBER_FUNCS_BOOL(debug_mask_secrets, OIDC_DEFAULT_DEBUG_MASK_SECRETS)

#define OIDC_DEFAULT_CLAIM_DELIMITER ","
OIDC_CFG_MEMBER_FUNCS_STR_DEF(claim_delimiter, NULL, OIDC_DEFAULT_CLAIM_DELIMITER)

OIDC_CFG_MEMBER_FUNCS_STR_DEF(metrics_path, NULL, NULL)

#define OIDC_DEFAULT_LOGOUT_X_FRAME_OPTIONS "DENY"
OIDC_CFG_MEMBER_FUNCS_STR_DEF(logout_x_frame_options, NULL, OIDC_DEFAULT_LOGOUT_X_FRAME_OPTIONS)

#define OIDC_STATE_TIMEOUT_MIN 1
#define OIDC_STATE_TIMEOUT_MAX (3600 * 24 * 30)
#define OIDC_DEFAULT_STATE_TIMEOUT 300

OIDC_CFG_MEMBER_FUNCS_INT(state_timeout, OIDC_STATE_TIMEOUT_MIN, OIDC_STATE_TIMEOUT_MAX, OIDC_DEFAULT_STATE_TIMEOUT)

#define OIDC_SESSION_CLIENT_COOKIE_CHUNK_SIZE_MIN 256
#define OIDC_SESSION_CLIENT_COOKIE_CHUNK_SIZE_MAX (1024 * 64)
#define OIDC_DEFAULT_SESSION_CLIENT_COOKIE_CHUNK_SIZE 4000

/* NB: 0 is accepted on top of the range and means "never chunk", which is what the
 * runtime already implements in oidc_http_set_chunked_cookie() */
OIDC_CFG_MEMBER_FUNCS_INT_EXT(session_cookie_chunk_size,
			      oidc_cfg_parse_int_min_max_or_zero(cmd->pool, arg, &v,
								 OIDC_SESSION_CLIENT_COOKIE_CHUNK_SIZE_MIN,
								 OIDC_SESSION_CLIENT_COOKIE_CHUNK_SIZE_MAX),
			      OIDC_DEFAULT_SESSION_CLIENT_COOKIE_CHUNK_SIZE)

#define OIDC_PROVIDER_METADATA_REFRESH_INTERVAL_MIN 30
#define OIDC_PROVIDER_METADATA_REFRESH_INTERVAL_MAX (3600 * 24 * 365)
#define OIDC_DEFAULT_PROVIDER_METADATA_REFRESH_INTERVAL 0

OIDC_CFG_MEMBER_FUNCS_INT(provider_metadata_refresh_interval, OIDC_PROVIDER_METADATA_REFRESH_INTERVAL_MIN,
			  OIDC_PROVIDER_METADATA_REFRESH_INTERVAL_MAX, OIDC_DEFAULT_PROVIDER_METADATA_REFRESH_INTERVAL)

#define OIDC_CFG_MEMBER_FUNCS_HASHTABLE(member)                                                                        \
	const char *oidc_cmd_##member##_set(cmd_parms *cmd, void *ptr, const char *arg) {                              \
		oidc_cfg_t *cfg =                                                                                      \
		    (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);              \
		if (cfg->member == NULL)                                                                               \
			cfg->member = apr_hash_make(cmd->pool);                                                        \
		apr_hash_set(cfg->member, arg, APR_HASH_KEY_STRING, arg);                                              \
		return NULL;                                                                                           \
	}                                                                                                              \
                                                                                                                       \
	OIDC_CFG_MEMBER_FUNC_GET(member, apr_hash_t *)

OIDC_CFG_MEMBER_FUNCS_HASHTABLE(white_listed_claims)
OIDC_CFG_MEMBER_FUNCS_HASHTABLE(black_listed_claims)
OIDC_CFG_MEMBER_FUNCS_HASHTABLE(redirect_urls_allowed)
OIDC_CFG_MEMBER_FUNCS_HASHTABLE(discover_issuers_allowed)

#define OIDC_CFG_MEMBER_FUNCS_ABS_OR_REL_URI(member)                                                                   \
	const char *oidc_cmd_##member##_set(cmd_parms *cmd, void *ptr, const char *arg) {                              \
		oidc_cfg_t *cfg =                                                                                      \
		    (oidc_cfg_t *)ap_get_module_config(cmd->server->module_config, &auth_openidc_module);              \
		const char *rv = oidc_cfg_parse_relative_or_absolute_url(cmd->pool, arg, &cfg->member);                \
		return OIDC_CONFIG_DIR_RV(cmd, rv);                                                                    \
	}                                                                                                              \
                                                                                                                       \
	OIDC_CFG_MEMBER_FUNC_GET(member, const char *)

OIDC_CFG_MEMBER_FUNCS_ABS_OR_REL_URI(redirect_uri)
OIDC_CFG_MEMBER_FUNCS_ABS_OR_REL_URI(default_sso_url)
OIDC_CFG_MEMBER_FUNCS_ABS_OR_REL_URI(default_slo_url)

typedef struct oidc_cfg_cleanup_ctx_t {
	oidc_cfg_t *cfg;
	apr_pool_t *pool;
	server_rec *svr;
} oidc_cfg_cleanup_ctx_t;

/*
 * destroy a server config record and its members
 */
apr_byte_t oidc_cfg_server_destroy(apr_pool_t *pool, server_rec *s, oidc_cfg_t *cfg) {
	if ((cfg->cache.impl) && (cfg->cache.impl->destroy))
		cfg->cache.impl->destroy(pool, s);
	cfg->cache.impl = NULL;
	oidc_cfg_provider_destroy(cfg->provider);
	cfg->provider = NULL;
	oidc_cfg_oauth_destroy(cfg->oauth);
	cfg->oauth = NULL;
	oidc_jwk_list_destroy(cfg->public_keys);
	cfg->public_keys = NULL;
	oidc_jwk_list_destroy(cfg->private_keys);
	cfg->private_keys = NULL;
	return TRUE;
}

static apr_status_t oidc_cfg_server_cleanup(void *data) {
	oidc_cfg_cleanup_ctx_t *ctx = (oidc_cfg_cleanup_ctx_t *)data;
	oidc_cfg_t *cfg = ctx->cfg;
	return oidc_cfg_server_destroy(ctx->pool, ctx->svr, cfg) ? APR_SUCCESS : APR_EGENERAL;
}

static oidc_cfg_t *oidc_cfg_server_alloc(apr_pool_t *pool, server_rec *s) {
	oidc_cfg_t *c = apr_pcalloc(pool, sizeof(oidc_cfg_t));
	oidc_cfg_cleanup_ctx_t *ctx = apr_pcalloc(pool, sizeof(oidc_cfg_cleanup_ctx_t));
	ctx->cfg = c;
	// pconf pool used at destruction time
	ctx->pool = pool;
	ctx->svr = s;
	// register a cleanup handler on the config pool to handle graceful restarts without increasing
	// memory consumption; this MUST be a *pre*-cleanup: libapr registers its own regular cleanups
	// for the cache global mutex's POSIX semaphore and the shm segment on this same pool when they
	// are created at post_config time, and regular cleanups run in reverse order of registration,
	// so a regular registration here (which happens much earlier, at config-parse time) would run
	// only AFTER libapr has already closed/unmapped those objects, making the mutex unlock/destroy
	// in oidc_cache_shm_destroy() dereference an unmapped semaphore and segfault the parent process
	// on a graceful restart; pre-cleanups are guaranteed to run before all regular cleanups
	apr_pool_pre_cleanup_register(pool, ctx, oidc_cfg_server_cleanup);
	return c;
}

/*
 * create a new server config record with defaults
 */
#define OIDC_SVR_M_CREATE_PTR(type, name) c->name = NULL;
#define OIDC_SVR_M_CREATE_INT(name) c->name = OIDC_CONFIG_POS_INT_UNSET;
#define OIDC_SVR_M_MERGE_PTR(type, name) c->name = _oidc_cfg_merge_ptr(add->name, base->name);
#define OIDC_SVR_M_MERGE_INT(name) c->name = _oidc_cfg_merge_pos_int(add->name, base->name);

void *oidc_cfg_server_create(apr_pool_t *pool, server_rec *svr) {
	oidc_cfg_t *c = oidc_cfg_server_alloc(pool, svr);
	c->svr = svr;

	c->merged = FALSE;
	/* nothing to inherit from: whatever OIDCRedirectURI this config ends up with is its own */
	c->redirect_uri_inherited = FALSE;

	OIDC_SVR_CFG_SIMPLE_MEMBERS(OIDC_SVR_M_CREATE_PTR, OIDC_SVR_M_CREATE_INT)
	c->public_keys = NULL;
	c->private_keys = NULL;

	c->provider = oidc_cfg_provider_create(pool);
	c->oauth = oidc_cfg_oauth_create(pool);
	oidc_cfg_cache_create_server_config(c);

	c->http_timeout_long.request_timeout = OIDC_CONFIG_POS_INT_UNSET;
	c->http_timeout_long.connect_timeout = OIDC_DEFAULT_HTTP_CONNECT_TIMEOUT_LONG;
	c->http_timeout_long.retries = OIDC_DEFAULT_HTTP_RETRIES_LONG;
	c->http_timeout_long.retry_interval = OIDC_DEFAULT_HTTP_RETRY_INTERVAL_LONG;
	c->http_timeout_short.request_timeout = OIDC_CONFIG_POS_INT_UNSET;
	c->http_timeout_short.connect_timeout = OIDC_DEFAULT_HTTP_CONNECT_TIMEOUT_SHORT;
	c->http_timeout_short.retries = OIDC_DEFAULT_HTTP_RETRIES_SHORT;
	c->http_timeout_short.retry_interval = OIDC_DEFAULT_HTTP_RETRY_INTERVAL_SHORT;

	c->remote_user_claim.claim_name = NULL;
	c->remote_user_claim.reg_exp = NULL;
	c->remote_user_claim.replace = NULL;

	c->outgoing_proxy.host_port = NULL;
	c->outgoing_proxy.username_password = NULL;
	c->outgoing_proxy.auth_type = OIDC_CONFIG_POS_INT_UNSET;

	c->crypto_passphrase.secret1 = NULL;
	c->crypto_passphrase.secret2 = NULL;
	c->crypto_passphrase.derived_key1_set = FALSE;
	c->crypto_passphrase.derived_key2_set = FALSE;

	c->info_hook_data = NULL;
	c->metrics_hook_data = NULL;

	c->black_listed_claims = NULL;
	c->white_listed_claims = NULL;
	c->filter_claims_expr = NULL;

	c->redirect_urls_allowed = NULL;
	c->discover_issuers_allowed = NULL;

	return c;
}

/*
 * merge an http_timeout struct: "add" wins as a whole if it has been configured
 */
static void _oidc_cfg_merge_http_timeout(oidc_http_timeout_t *c, const oidc_http_timeout_t *add,
					 const oidc_http_timeout_t *base) {
	const oidc_http_timeout_t *src = add->request_timeout != OIDC_CONFIG_POS_INT_UNSET ? add : base;
	c->request_timeout = src->request_timeout;
	c->connect_timeout = src->connect_timeout;
	c->retries = src->retries;
	c->retry_interval = src->retry_interval;
}

/*
 * merge the remote_user_claim struct: "add" wins as a whole if it has been configured
 */
static void _oidc_cfg_merge_remote_user_claim(oidc_remote_user_claim_t *c, const oidc_remote_user_claim_t *add,
					      const oidc_remote_user_claim_t *base) {
	const oidc_remote_user_claim_t *src = add->claim_name != NULL ? add : base;
	c->claim_name = src->claim_name;
	c->reg_exp = src->reg_exp;
	c->replace = src->replace;
}

/*
 * merge the outgoing_proxy struct: "add" wins as a whole if it has been configured
 */
static void _oidc_cfg_merge_outgoing_proxy(oidc_http_outgoing_proxy_t *c, const oidc_http_outgoing_proxy_t *add,
					   const oidc_http_outgoing_proxy_t *base) {
	const oidc_http_outgoing_proxy_t *src = add->host_port != NULL ? add : base;
	c->host_port = src->host_port;
	c->username_password = src->username_password;
	c->auth_type = src->auth_type;
}

/*
 * merge the crypto_passphrase struct: "add" wins as a whole if it has been configured
 */
static void _oidc_cfg_merge_crypto_passphrase(oidc_crypto_passphrase_t *c, const oidc_crypto_passphrase_t *add,
					      const oidc_crypto_passphrase_t *base) {
	const oidc_crypto_passphrase_t *src = add->secret1 != NULL ? add : base;
	c->secret1 = src->secret1;
	c->secret2 = src->secret2;
	/* Keep derived keys paired with the selected secrets, regardless of merge/derivation order. */
	c->derived_key1_set = src->derived_key1_set;
	if (src->derived_key1_set)
		_oidc_memcpy(c->derived_key1, src->derived_key1, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
	c->derived_key2_set = src->derived_key2_set;
	if (src->derived_key2_set)
		_oidc_memcpy(c->derived_key2, src->derived_key2, OIDC_CRYPTO_PASSPHRASE_DERIVED_KEY_LEN);
}

/*
 * merge a new server config with a base one
 */
void *oidc_cfg_server_merge(apr_pool_t *pool, void *BASE, void *ADD) {
	const oidc_cfg_t *base = (const oidc_cfg_t *)BASE;
	const oidc_cfg_t *add = (const oidc_cfg_t *)ADD;

	oidc_cfg_t *c = oidc_cfg_server_alloc(pool, add->svr);

	c->provider = oidc_cfg_provider_create(pool);
	c->oauth = oidc_cfg_oauth_create(pool);

	c->merged = TRUE;
	/* record where the merged redirect_uri came from while "add" (this vhost's own config) and
	 * "base" can still be told apart: the merge below collapses them into a single value */
	c->redirect_uri_inherited = (add->redirect_uri == NULL) && (base->redirect_uri != NULL);

	oidc_cfg_provider_merge(pool, c->provider, base->provider, add->provider);
	oidc_cfg_oauth_merge(pool, c->oauth, base->oauth, add->oauth);
	oidc_cfg_cache_merge_server_config(c, base, add);

	OIDC_SVR_CFG_SIMPLE_MEMBERS(OIDC_SVR_M_MERGE_PTR, OIDC_SVR_M_MERGE_INT)
	c->public_keys = oidc_jwk_list_copy(pool, _oidc_cfg_merge_ptr(add->public_keys, base->public_keys));
	c->private_keys = oidc_jwk_list_copy(pool, _oidc_cfg_merge_ptr(add->private_keys, base->private_keys));

	_oidc_cfg_merge_http_timeout(&c->http_timeout_long, &add->http_timeout_long, &base->http_timeout_long);
	_oidc_cfg_merge_http_timeout(&c->http_timeout_short, &add->http_timeout_short, &base->http_timeout_short);

	_oidc_cfg_merge_remote_user_claim(&c->remote_user_claim, &add->remote_user_claim, &base->remote_user_claim);

	_oidc_cfg_merge_outgoing_proxy(&c->outgoing_proxy, &add->outgoing_proxy, &base->outgoing_proxy);
	_oidc_cfg_merge_crypto_passphrase(&c->crypto_passphrase, &add->crypto_passphrase, &base->crypto_passphrase);

	c->info_hook_data = _oidc_cfg_merge_ptr(add->info_hook_data, base->info_hook_data);
	c->metrics_hook_data = _oidc_cfg_merge_ptr(add->metrics_hook_data, base->metrics_hook_data);

	c->black_listed_claims = _oidc_cfg_merge_ptr(add->black_listed_claims, base->black_listed_claims);
	c->white_listed_claims = _oidc_cfg_merge_ptr(add->white_listed_claims, base->white_listed_claims);
	c->filter_claims_expr = _oidc_cfg_merge_ptr(add->filter_claims_expr, base->filter_claims_expr);

	c->redirect_urls_allowed = _oidc_cfg_merge_ptr(add->redirect_urls_allowed, base->redirect_urls_allowed);
	c->discover_issuers_allowed =
	    _oidc_cfg_merge_ptr(add->discover_issuers_allowed, base->discover_issuers_allowed);

	return c;
}

/*
 * Build a read-only request view with a private OAuth struct, allowing metadata endpoints to
 * change without mutating shared server state. Other members remain shared.
 */
oidc_cfg_t *oidc_cfg_request_view(apr_pool_t *pool, const oidc_cfg_t *c) {
	oidc_cfg_t *rc = apr_pmemdup(pool, c, sizeof(*c));
	rc->oauth = oidc_cfg_oauth_shallow_copy(pool, c->oauth);
	return rc;
}

/*
 * initialize before the post config handler runs
 */
void oidc_pre_config_init(void) {
#if OPENSSL_API_COMPAT < 0x10100000L
	ERR_load_crypto_strings();
	OpenSSL_add_all_algorithms();
	OpenSSL_add_all_digests();
#else
	OPENSSL_init_crypto(0, NULL);
#endif
}

oidc_provider_t *oidc_cfg_provider_get(oidc_cfg_t *cfg) {
	return cfg->provider;
}

int oidc_cfg_merged_get(const oidc_cfg_t *cfg) {
	return cfg->merged;
}

int oidc_cfg_redirect_uri_inherited_get(const oidc_cfg_t *cfg) {
	return cfg->redirect_uri_inherited;
}

static oidc_cache_mutex_t *_oidc_refresh_mutex = NULL;

oidc_cache_mutex_t *oidc_cfg_refresh_mutex_get(const oidc_cfg_t *cfg) {
	return _oidc_refresh_mutex;
}

int oidc_cfg_post_config(apr_pool_t *pool, oidc_cfg_t *cfg, server_rec *s) {
	if (cfg->cache.impl == NULL)
		cfg->cache.impl = &oidc_cache_shm;
	if ((cfg->cache.impl->post_config != NULL) && (cfg->cache.impl->post_config(pool, s) != OK))
		return HTTP_INTERNAL_SERVER_ERROR;
	if (_oidc_refresh_mutex == NULL) {
		// NB: use the process pool here as the mutex is a process-wide singleton
		_oidc_refresh_mutex = oidc_cache_mutex_create(s->process->pool, TRUE);
		if (oidc_cache_mutex_post_config(s->process->pool, s, _oidc_refresh_mutex, "refresh") != TRUE)
			return HTTP_INTERNAL_SERVER_ERROR;
	}
	if ((cfg->metrics_hook_data != NULL) && (oidc_metrics_post_config(pool, s) != TRUE))
		return HTTP_INTERNAL_SERVER_ERROR;
	return OK;
}

void oidc_cfg_child_init(apr_pool_t *pool, const oidc_cfg_t *cfg, server_rec *s) {
	if ((cfg->cache.impl->child_init != NULL) && (cfg->cache.impl->child_init(pool, s) != APR_SUCCESS))
		oidc_serror(s, "cfg->cache->child_init failed");
	if ((_oidc_refresh_mutex != NULL) && (oidc_cache_mutex_child_init(pool, s, _oidc_refresh_mutex) != APR_SUCCESS))
		oidc_serror(s, "oidc_cache_mutex_child_init on refresh mutex failed");
	if ((cfg->metrics_hook_data != NULL) && (oidc_metrics_child_init(pool, s) != APR_SUCCESS))
		oidc_serror(s, "oidc_metrics_cache_child_init failed");
}

void oidc_cfg_process_cleanup(oidc_cfg_t *cfg, server_rec *s) {
	if (_oidc_refresh_mutex != NULL) {
		if (oidc_cache_mutex_destroy(s, _oidc_refresh_mutex) != TRUE) {
			oidc_serror(s, "oidc_cache_mutex_destroy on refresh mutex failed");
		}
		// this is a singleton, make sure we call destroy only once
		_oidc_refresh_mutex = NULL;
	}
	if (cfg->metrics_hook_data != NULL) {
		if (oidc_metrics_cleanup(s) != APR_SUCCESS) {
			oidc_serror(s, "oidc_metrics_cleanup failed");
		}
		cfg->metrics_hook_data = NULL;
	}
}
