/*
 * 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 "handle/handle.h"
#include "metrics.h"
#include "mod_auth_openidc.h"
#include "proto/proto.h"
#include "state.h"
#include "util/request_state.h"
#include "util/util.h"
#include "util/util_cfg.h"

apr_byte_t oidc_request_check_cookie_domain(request_rec *r, const oidc_cfg_t *c, const char *original_url) {
	/*
	 * printout errors if Cookie settings are not going to work
	 */
	apr_uri_t o_uri;
	_oidc_memset(&o_uri, 0, sizeof(apr_uri_t));
	apr_uri_t r_uri;
	_oidc_memset(&r_uri, 0, sizeof(apr_uri_t));
	apr_uri_parse(r->pool, original_url, &o_uri);
	apr_uri_parse(r->pool, oidc_util_url_redirect_uri(r, c), &r_uri);
	if ((_oidc_strnatcasecmp(o_uri.scheme, r_uri.scheme) != 0) &&
	    (_oidc_strnatcasecmp(r_uri.scheme, "https") == 0)) {
		oidc_error(r,
			   "the URL scheme (%s) of the configured " OIDCRedirectURI
			   " does not match the URL scheme of the URL being accessed (%s): the \"state\" and "
			   "\"session\" cookies will not be shared between the two!",
			   r_uri.scheme, o_uri.scheme);
		return FALSE;
	}

	if (oidc_cfg_cookie_domain_get(c) == NULL) {
		if (oidc_util_hostname_endswith(o_uri.hostname, r_uri.hostname) == FALSE) {
			oidc_error(r,
				   "the URL hostname (%s) of the configured " OIDCRedirectURI
				   " does not match the URL hostname of the URL being accessed (%s): the "
				   "\"state\" and \"session\" cookies will not be shared between the two!",
				   r_uri.hostname, o_uri.hostname);
			OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_REQUEST_ERROR_URL);
			return FALSE;
		}
	} else {
		if (!oidc_util_cookie_domain_valid(o_uri.hostname, oidc_cfg_cookie_domain_get(c))) {
			oidc_error(r,
				   "the domain (%s) configured in " OIDCCookieDomain
				   " does not match the URL hostname (%s) of the URL being accessed (%s): setting "
				   "\"state\" and \"session\" cookies will not work!!",
				   oidc_cfg_cookie_domain_get(c), o_uri.hostname, original_url);
			OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_REQUEST_ERROR_URL);
			return FALSE;
		}
	}

	return TRUE;
}

static const char *oidc_request_samesite_state_cookie(const request_rec *r, const struct oidc_cfg_t *c) {
	const char *rv = NULL;
	switch (oidc_cfg_cookie_same_site_state_get(c)) {
	case OIDC_SAMESITE_COOKIE_STRICT:
	case OIDC_SAMESITE_COOKIE_LAX:
		rv = OIDC_HTTP_COOKIE_SAMESITE_LAX;
		break;
	case OIDC_SAMESITE_COOKIE_NONE:
		rv = OIDC_HTTP_COOKIE_SAMESITE_NONE(c, r);
		break;
	case OIDC_SAMESITE_COOKIE_DISABLED:
	default:
		break;
	}
	return rv;
}

/*
 * set the state that is maintained between an authorization request and an authorization response
 * in a cookie in the browser that is cryptographically bound to that state
 */
static int oidc_request_authorization_set_cookie(request_rec *r, const oidc_cfg_t *c, const char *state,
						 const oidc_proto_state_t *proto_state) {
	/*
	 * create a cookie consisting of 8 elements:
	 * random value, original URL, original method, issuer, response_type, response_mod, prompt and timestamp
	 * encoded as JSON, encrypting the resulting JSON value
	 */
	const char *cookieValue = oidc_proto_state_to_cookie(r, c, proto_state);
	if (cookieValue == NULL)
		return HTTP_INTERNAL_SERVER_ERROR;

	/*
	 * clean expired state cookies to avoid pollution and optionally
	 * try to avoid the number of state cookies exceeding a max
	 */
	int number_of_cookies =
	    oidc_state_cookies_clean_expired(r, c, NULL, oidc_cfg_delete_oldest_state_cookies_get(c));
	int max_number_of_cookies = oidc_cfg_max_number_of_state_cookies_get(c);
	if ((max_number_of_cookies > 0) && (number_of_cookies >= max_number_of_cookies)) {
		oidc_warn(r,
			  "the number of existing, valid state cookies (%d) has exceeded the limit (%d), no additional "
			  "authorization request + state cookie can be generated, aborting the request",
			  number_of_cookies, max_number_of_cookies);
		return HTTP_SERVICE_UNAVAILABLE;
	}

	/* assemble the cookie name for the state cookie */
	const char *cookieName = oidc_state_cookie_name(r, state);

	/* set it as a cookie */
	oidc_http_set_cookie(r, cookieName, cookieValue, -1, oidc_request_samesite_state_cookie(r, c));

	return OK;
}
/* context structure for encoding parameters */
typedef struct oidc_request_form_post_ctx_t {
	request_rec *r;
	/* the "<input ...>" lines, joined once by the caller below; growing the whole body per parameter
	 * instead made the work quadratic in the number of parameters, which a request-controlled
	 * parameter count can turn into an OOM (OSS-Fuzz 551746349) */
	apr_array_header_t *inputs;
} oidc_request_form_post_ctx_t;

/*
 * add a key/value pair post parameter
 */
static int oidc_request_form_post_param_add(void *rec, const char *key, const char *value) {
	oidc_request_form_post_ctx_t *ctx = (oidc_request_form_post_ctx_t *)rec;
	oidc_debug(ctx->r, "processing: %s=%s", key, value);
	APR_ARRAY_PUSH(ctx->inputs, const char *) =
	    apr_psprintf(ctx->r->pool, "      <input type=\"hidden\" name=\"%s\" value=\"%s\">\n",
			 oidc_util_html_escape(ctx->r->pool, key), oidc_util_html_escape(ctx->r->pool, value));
	return 1;
}

/*
 * make the browser POST parameters through Javascript auto-submit
 */
static const char *oidc_request_html_post(request_rec *r, const char *url, const apr_table_t *params) {

	oidc_debug(r, "enter");

	oidc_request_form_post_ctx_t data = {
	    r, apr_array_make(r->pool, params ? apr_table_elts(params)->nelts : 0, sizeof(const char *))};
	apr_table_do(oidc_request_form_post_param_add, &data, params, NULL);

	/* the action is the provider's authorization endpoint, i.e. it comes from provider
	 * metadata rather than from us; escape it like the parameters added above already are */
	return apr_psprintf(r->pool,
			    "    <p>Submitting Authentication Request...</p>\n"
			    "    <form method=\"post\" action=\"%s\">\n"
			    "      <p>\n"
			    "%s"
			    "      </p>\n"
			    "    </form>\n",
			    oidc_util_html_escape(r->pool, url), apr_array_pstrcat(r->pool, data.inputs, '\0'));
}
/*
 * send the authentication request via an HTML form auto-POST page
 */
static int oidc_request_auth_send_post(request_rec *r, const struct oidc_provider_t *provider,
				       const apr_table_t *params) {
	/* construct a HTML POST auto-submit page with the authorization request parameters */
	const char *html_body =
	    oidc_request_html_post(r, oidc_cfg_provider_authorization_endpoint_url_get(provider), params);

	/* signal this to the content handler */
	return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_AUTHN_POST, "Submitting...", NULL,
					   "HTMLFormElement.prototype.submit.call(document.forms[0])", html_body);
}

/*
 * send the authentication request via an HTTP redirect (or a Javascript-based preserve page)
 */
static int oidc_request_auth_send_get(request_rec *r, const struct oidc_provider_t *provider,
				      const apr_table_t *params) {

	/* construct the full authorization request URL */
	const char *authorization_request =
	    oidc_http_query_encoded_url(r, oidc_cfg_provider_authorization_endpoint_url_get(provider), params);

	char *javascript = NULL;

	/* see if we need to preserve POST parameters through Javascript/HTML5 storage */
	if (oidc_response_post_preserve_javascript(r, authorization_request, &javascript, NULL) == FALSE) {
		/* add the redirect location header */
		oidc_http_hdr_out_location_set(r, authorization_request);
		/* and tell Apache to return an HTTP Redirect (302) message */
		return HTTP_MOVED_TEMPORARILY;
	}

	// NB: if a template is in use, we should not override
	// OIDC_REQUEST_STATE_KEY_HTTP with OIDC_REQUEST_STATE_KEY_AUTHN_PRESERVE
	if (oidc_request_state_get(r, OIDC_REQUEST_STATE_KEY_HTTP) != NULL)
		return OK;

	/* signal this to the content handler */
	return oidc_util_html_content_prep(r, OIDC_REQUEST_STATE_KEY_AUTHN_PRESERVE, "Preserving...", javascript,
					   "preserveOnLoad()", "<p>Preserving...</p>");
}

/*
 * send an OpenID Connect authorization request to the specified provider
 */
int oidc_request_auth(request_rec *r, oidc_cfg_t *cfg, const struct oidc_provider_t *provider, const char *login_hint,
		      const char *redirect_uri, const char *state, oidc_proto_state_t *proto_state,
		      const char *id_token_hint, const char *code_challenge, const char *auth_request_params,
		      const char *path_scope) {
	int rv;

	/* log some stuff */
	oidc_debug(r,
		   "enter, issuer=%s, redirect_uri=%s, state=%s, proto_state=%s, code_challenge=%s, "
		   "auth_request_params=%s, path_scope=%s",
		   oidc_cfg_provider_issuer_get(provider), redirect_uri, state,
		   oidc_proto_state_to_string(r, proto_state), code_challenge, auth_request_params, path_scope);

	if (oidc_cfg_provider_client_id_get(provider) == NULL) {
		oidc_error(r, "no Client ID set for the provider: perhaps you are accessing an endpoint protected with "
			      "\"AuthType openid-connect\" instead of \"AuthType oauth20\"?)");
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* assemble parameters to call the authorization endpoint */
	apr_table_t *params = apr_table_make(r->pool, 4);
	oidc_proto_request_auth_params_set(r, cfg, provider, login_hint, redirect_uri, state, proto_state,
					   id_token_hint, code_challenge, auth_request_params, path_scope, params);

	/* send the full authentication request via POST, PAR or GET */
	switch (oidc_proto_profile_auth_request_method_get(provider)) {
	case OIDC_AUTH_REQUEST_METHOD_POST:
		rv = oidc_request_auth_send_post(r, provider, params);
		break;
	case OIDC_AUTH_REQUEST_METHOD_PAR:
		rv = oidc_proto_request_auth_push(r, cfg, provider, params);
		break;
	case OIDC_AUTH_REQUEST_METHOD_GET:
		rv = oidc_request_auth_send_get(r, provider, params);
		break;
	default:
		oidc_error(r, "oidc_cfg_provider_auth_request_method_get(provider) set to an unknown value: %d",
			   oidc_proto_profile_auth_request_method_get(provider));
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* cleanup */
	oidc_proto_state_destroy(proto_state);

	/* no cache */
	oidc_http_hdr_err_out_add(r, OIDC_HTTP_HDR_CACHE_CONTROL, "no-cache, no-store, max-age=0");

	/* log our exit code */
	oidc_debug(r, "return: %d", rv);

	return rv;
}

/*
 * authenticate the user to the selected OP, if the OP is not selected yet perform discovery first
 */
int oidc_request_authenticate_user(request_rec *r, oidc_cfg_t *c, oidc_provider_t *provider, const char *original_url,
				   const char *login_hint, const char *id_token_hint, const char *prompt,
				   const char *auth_request_params, const char *path_scope) {

	int rc;

	OIDC_METRICS_TIMING_START(r, c);

	oidc_debug(r, "enter");

	if (provider == NULL) {

		// NB: discovery is deferred to the content handler below rather than via an explicit redirect to a
		// discovery endpoint
		if (oidc_cfg_metadata_dir_get(c) != NULL) {
			/*
			 * No authentication done but request not allowed without authentication
			 * by setting r->user
			 */
			oidc_request_state_set(r, OIDC_REQUEST_STATE_KEY_DISCOVERY, "");
			oidc_debug(r, "defer discovery to the content handler, setting r->user=\"\"");
			r->user = "";

			return OK;
		}

		/* we're not using multiple OP's configured in a metadata directory, pick the statically configured OP
		 */
		if (oidc_provider_static_config(r, c, &provider) == FALSE) {
			OIDC_METRICS_COUNTER_INC(r, c, OM_AUTHN_RESPONSE_ERROR_PROVIDER);
			return HTTP_INTERNAL_SERVER_ERROR;
		}
	}

	/* generate the random nonce value that correlates requests and responses */
	char *nonce = NULL;
	if (oidc_proto_nonce_gen(r, &nonce) == FALSE)
		return HTTP_INTERNAL_SERVER_ERROR;

	char *pkce_state = NULL;
	char *code_challenge = NULL;

	if ((oidc_util_spaced_string_contains(r->pool, oidc_cfg_provider_response_type_get(provider),
					      OIDC_PROTO_CODE) == TRUE) &&
	    (oidc_proto_profile_pkce_get(provider) != &oidc_pkce_none)) {

		/* generate the code verifier value that correlates authorization requests and code exchange requests */
		if (oidc_proto_profile_pkce_get(provider)->state(r, &pkce_state) == FALSE)
			return HTTP_INTERNAL_SERVER_ERROR;

		/* generate the PKCE code challenge */
		if (oidc_proto_profile_pkce_get(provider)->challenge(r, pkce_state, &code_challenge) == FALSE)
			return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* create the state between request/response */
	oidc_proto_state_t *proto_state = oidc_proto_state_new();
	oidc_proto_state_set_original_url(proto_state, original_url);

	if (oidc_proto_state_get_original_url(proto_state) == NULL) {
		oidc_error(
		    r, "could not store the current URL in the state: most probably you need to ensure that it does "
		       "not contain unencoded Unicode characters e.g. by forcing IE 11 to encode all URL characters");
		oidc_proto_state_destroy(proto_state);
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	oidc_proto_state_set_original_method(proto_state, oidc_original_request_method(r, c, TRUE));
	oidc_proto_state_set_issuer(proto_state, oidc_cfg_provider_issuer_get(provider));
	oidc_proto_state_set_response_type(proto_state, oidc_cfg_provider_response_type_get(provider));
	oidc_proto_state_set_nonce(proto_state, nonce);
	oidc_proto_state_set_timestamp_now(proto_state);
	if (oidc_cfg_provider_response_mode_get(provider))
		oidc_proto_state_set_response_mode(proto_state, oidc_cfg_provider_response_mode_get(provider));
	if (prompt)
		oidc_proto_state_set_prompt(proto_state, prompt);
	if (pkce_state)
		oidc_proto_state_set_pkce_state(proto_state, pkce_state);
	/* Preserve per-path settings only when session management may need them for silent reauthentication. */
	if (oidc_cfg_provider_check_session_iframe_get(provider) != NULL) {
		if (auth_request_params)
			oidc_proto_state_set_auth_request_params(proto_state, auth_request_params);
		if (path_scope)
			oidc_proto_state_set_path_scope(proto_state, path_scope);
	}

	/* get a hash value that fingerprints the browser concatenated with the random input */
	const char *state = oidc_state_browser_fingerprint(r, c, nonce);

	/*
	 * create state that restores the context when the authorization response comes in
	 * and cryptographically bind it to the browser
	 */
	rc = oidc_request_authorization_set_cookie(r, c, state, proto_state);
	if (rc != OK) {
		oidc_proto_state_destroy(proto_state);
		return rc;
	}

	if (oidc_request_check_cookie_domain(r, c, original_url) == FALSE) {
		oidc_proto_state_destroy(proto_state);
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* send off to the OpenID Connect Provider */
	rc = oidc_request_auth(r, c, provider, login_hint, oidc_util_url_redirect_uri(r, c), state, proto_state,
			       id_token_hint, code_challenge, auth_request_params, path_scope);

	OIDC_METRICS_TIMING_ADD(r, c, OM_AUTHN_REQUEST);

	return rc;
}
