/*
 * 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 "mod_auth_openidc.h"
#include "proto/proto.h"
#include "util/util.h"

#include <openssl/crypto.h>

/*
 * if a nonce was passed in the authorization request (and stored in the browser state),
 * check that it matches the nonce value in the id_token payload
 */
// non-static for test.c
apr_byte_t oidc_proto_idtoken_validate_nonce(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
					     const char *nonce, const oidc_jwt_t *jwt) {

	oidc_jose_error_t err;

	/* see if we have this nonce cached already */
	char *replay = NULL;
	if (oidc_cache_get_nonce(r, nonce, &replay) == FALSE)
		oidc_warn(r, "cache lookup for nonce replay detection failed; a backend error is treated as "
			     "\"not seen\", so replay protection is not enforced for this request");
	if (replay != NULL) {
		oidc_error(r,
			   "the nonce value (%s) passed in the browser state was found in the cache already; possible "
			   "replay attack!?",
			   nonce);
		return FALSE;
	}

	/* get the "nonce" value in the id_token payload */
	char *j_nonce = NULL;
	if (oidc_jose_get_string(r->pool, jwt->payload.value.json, OIDC_CLAIM_NONCE, TRUE, &j_nonce, &err) == FALSE) {
		oidc_error(r, "id_token JSON payload did not contain a \"%s\" string: %s", OIDC_CLAIM_NONCE,
			   oidc_jose_e2s(r->pool, err));
		return FALSE;
	}

	/* see if the nonce in the id_token matches the one that we sent in the authorization request */
	if (_oidc_strcmp(nonce, j_nonce) != 0) {
		oidc_error(
		    r, "the nonce value (%s) in the id_token did not match the one stored in the browser session (%s)",
		    j_nonce, nonce);
		return FALSE;
	}

	/*
	 * nonce cache duration (replay prevention window) is the 2x the configured
	 * slack on the timestamp (+-) for token issuance plus 10 seconds for safety
	 */
	apr_time_t nonce_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 */
	if (oidc_cache_set_nonce(r, nonce, nonce, apr_time_now() + nonce_cache_duration) == FALSE)
		oidc_warn(r, "failed to store nonce in the cache; a subsequent replay of this id_token may go "
			     "undetected");

	oidc_debug(r, "nonce \"%s\" validated successfully and is now cached for %" APR_TIME_T_FMT " seconds", nonce,
		   apr_time_sec(nonce_cache_duration));

	return TRUE;
}

#define OIDC_PROTO_IDTOKEN_AUD_CLIENT_ID_SPECIAL_VALUE "@"

/*
 * validate that the "azp" claim, when present, matches the configured client_id
 */
static apr_byte_t oidc_proto_idtoken_validate_azp(request_rec *r, const oidc_provider_t *provider, const char *azp) {
	/*
	 * the "azp" claim is only needed when the id_token has a single audience value and that audience
	 * is different than the authorized party; it MAY be included even when the authorized party is
	 * the same as the sole audience.
	 */
	if ((azp == NULL) || (_oidc_strcmp(azp, oidc_cfg_provider_client_id_get(provider)) == 0))
		return TRUE;
	oidc_error(r,
		   "the \"%s\" claim (%s) is present in the id_token, but is not equal to the configured "
		   "client_id (%s)",
		   OIDC_CLAIM_AZP, azp, oidc_cfg_provider_client_id_get(provider));
	return FALSE;
}

/*
 * resolve the special "@" audience value to the configured client_id (passthrough otherwise)
 */
static const char *oidc_proto_idtoken_aud_resolve(const oidc_provider_t *provider, const char *s_aud) {
	if (_oidc_strcmp(s_aud, OIDC_PROTO_IDTOKEN_AUD_CLIENT_ID_SPECIAL_VALUE) == 0)
		return oidc_cfg_provider_client_id_get(provider);
	return s_aud;
}

/*
 * validate a single-valued "aud" claim against either the client_id or the configured aud-values list
 */
static apr_byte_t oidc_proto_idtoken_validate_aud_string(request_rec *r, const oidc_provider_t *provider,
							 const apr_array_header_t *arr, const char *aud_value) {

	if (arr == NULL) {
		/* a single-valued audience must be equal to our client_id */
		if (_oidc_strcmp(aud_value, oidc_cfg_provider_client_id_get(provider)) == 0)
			return TRUE;
		oidc_error(r, "the configured client_id (%s) did not match the \"%s\" claim value (%s) in the id_token",
			   oidc_cfg_provider_client_id_get(provider), OIDC_CLAIM_AUD, aud_value);
		return FALSE;
	}

	for (int i = 0; i < arr->nelts; i++) {
		const char *s_aud = oidc_proto_idtoken_aud_resolve(provider, APR_ARRAY_IDX(arr, i, const char *));
		if (_oidc_strcmp(aud_value, s_aud) == 0)
			return TRUE;
	}

	oidc_error(r, "none of our configured audience values could be found in \"%s\" claim", OIDC_CLAIM_AUD);
	return FALSE;
}

/*
 * validate a multi-valued "aud" claim against either the client_id or the configured aud-values list
 */
static apr_byte_t oidc_proto_idtoken_validate_aud_array(request_rec *r, const oidc_provider_t *provider,
							const apr_array_header_t *arr, const oidc_json_t *aud,
							const char *azp) {
	const char *s_aud = NULL;

	if (arr == NULL) {
		if ((oidc_json_array_size(aud) > 1) && (azp == NULL))
			oidc_warn(r,
				  "the \"%s\" claim value in the id_token is an array with more than 1 element, but "
				  "\"%s\" claim is not present (a SHOULD in the spec...)",
				  OIDC_CLAIM_AUD, OIDC_CLAIM_AZP);

		if (oidc_json_array_has_value(r, aud, oidc_cfg_provider_client_id_get(provider)) == FALSE) {
			oidc_error(r,
				   "our configured client_id (%s) could not be found in the array of values for \"%s\" "
				   "claim",
				   oidc_cfg_provider_client_id_get(provider), OIDC_CLAIM_AUD);
			return FALSE;
		}
		return TRUE;
	}

	/* handle explicit and exhaustive configuration of acceptable audience values */
	for (int i = 0; i < arr->nelts; i++) {
		s_aud = oidc_proto_idtoken_aud_resolve(provider, APR_ARRAY_IDX(arr, i, const char *));
		if (oidc_json_array_has_value(r, aud, s_aud) == FALSE) {
			oidc_error(r,
				   "our configured audience value (%s) could not be found in the array of values for "
				   "\"%s\" claim",
				   APR_ARRAY_IDX(arr, i, const char *), OIDC_CLAIM_AUD);
			return FALSE;
		}
	}

	if (oidc_json_array_size(aud) > (size_t)arr->nelts) {
		oidc_error(r,
			   "our configured audience values are all present in the array of values for \"%s\" claim, "
			   "but there are other unknown/untrusted values included as well",
			   OIDC_CLAIM_AUD);
		return FALSE;
	}

	return TRUE;
}

/*
 * validate the "aud" and "azp" claims in the id_token payload
 */
apr_byte_t oidc_proto_idtoken_validate_aud_and_azp(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
						   const oidc_jwt_payload_t *id_token_payload) {
	char *azp = NULL;
	const oidc_json_t *aud = NULL;
	const apr_array_header_t *arr = NULL;

	oidc_jose_get_string(r->pool, id_token_payload->value.json, OIDC_CLAIM_AZP, FALSE, &azp, NULL);

	if (oidc_proto_idtoken_validate_azp(r, provider, azp) == FALSE)
		return FALSE;

	/* get the "aud" value from the JSON payload */
	aud = oidc_json_object_get(id_token_payload->value.json, OIDC_CLAIM_AUD);
	if (aud == NULL) {
		oidc_error(r, "id_token JSON payload did not contain an \"%s\" claim", OIDC_CLAIM_AUD);
		return FALSE;
	}

	arr = oidc_proto_profile_id_token_aud_values_get(r->pool, provider);

	if (oidc_json_is_string(aud))
		return oidc_proto_idtoken_validate_aud_string(r, provider, arr, oidc_json_string_value(aud));

	if (oidc_json_is_array(aud))
		return oidc_proto_idtoken_validate_aud_array(r, provider, arr, aud, azp);

	oidc_error(r, "id_token JSON payload \"%s\" claim is not a string nor an array", OIDC_CLAIM_AUD);
	return FALSE;
}

/*
 * check a provided hash value (at_hash|c_hash) against a corresponding hash calculated for a specified value and
 * algorithm
 */
static apr_byte_t oidc_proto_validate_hash(request_rec *r, const char *alg, const char *hash, const char *value,
					   const char *type) {

	char *calc = NULL;
	unsigned int calc_len = 0;
	unsigned int hash_len = oidc_jose_hash_length(alg) / 2;
	oidc_jose_error_t err;

	/* hash the provided access_token */
	if (oidc_jose_hash_string(r->pool, alg, value, &calc, &calc_len, &err) == FALSE) {
		oidc_error(r, "oidc_jose_hash_string failed: %s", oidc_jose_e2s(r->pool, err));
		return FALSE;
	}

	/* calculate the base64url-encoded value of the hash */
	char *decoded = NULL;
	int decoded_len = oidc_util_base64url_decode(r->pool, &decoded, hash);
	if (decoded_len <= 0) {
		oidc_error(r, "oidc_base64url_decode returned an error");
		return FALSE;
	}

	oidc_debug(r, "hash_len=%u, decoded_len=%d, calc_len=%u", hash_len, decoded_len, calc_len);

	/* compare the calculated hash against the provided hash; use a
	 * constant-time compare so the failure path can't leak information
	 * about how many leading bytes matched */
	if (((unsigned int)decoded_len != hash_len) || (calc_len < hash_len) ||
	    (CRYPTO_memcmp(decoded, calc, hash_len) != 0)) {
		oidc_error(r, "provided \"%s\" hash value (%s) does not match the calculated value", type, hash);
		return FALSE;
	}

	oidc_debug(r, "successfully validated the provided \"%s\" hash value (%s) against the calculated value", type,
		   hash);

	return TRUE;
}

/*
 * check a hash value in the id_token against the corresponding hash calculated over a provided value
 */
static apr_byte_t oidc_proto_validate_hash_value(request_rec *r, oidc_provider_t *provider, const oidc_jwt_t *jwt,
						 const char *response_type, const char *value, const char *key,
						 apr_array_header_t *required_for_flows) {

	/*
	 * get the hash value from the id_token
	 */
	char *hash = NULL;
	oidc_jose_get_string(r->pool, jwt->payload.value.json, key, FALSE, &hash, NULL);

	/*
	 * check if the hash was present
	 */
	if (hash == NULL) {

		/* no hash..., now see if the flow required it */
		for (int i = 0; i < required_for_flows->nelts; i++) {
			if (oidc_util_spaced_string_equals(r->pool, response_type,
							   APR_ARRAY_IDX(required_for_flows, i, const char *))) {
				oidc_warn(r, "flow is \"%s\", but no %s found in id_token", response_type, key);
				return FALSE;
			}
		}

		/* no hash but it was not required anyway */
		return TRUE;
	}

	/*
	 * we have a hash, validate it and return the result
	 */
	return oidc_proto_validate_hash(r, jwt->header.alg, hash, value, key);
}

/*
 * check the at_hash value in the id_token against the access_token
 */
apr_byte_t oidc_proto_idtoken_validate_access_token(request_rec *r, oidc_provider_t *provider, const oidc_jwt_t *jwt,
						    const char *response_type, const char *access_token) {
	apr_array_header_t *required_for_flows = apr_array_make(r->pool, 2, sizeof(const char *));
	APR_ARRAY_PUSH(required_for_flows, const char *) = OIDC_PROTO_RESPONSE_TYPE_IDTOKEN_TOKEN;
	APR_ARRAY_PUSH(required_for_flows, const char *) = OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN_TOKEN;
	if (oidc_proto_validate_hash_value(r, provider, jwt, response_type, access_token, OIDC_CLAIM_AT_HASH,
					   required_for_flows) == FALSE) {
		oidc_error(r, "could not validate access token against \"%s\" claim value", OIDC_CLAIM_AT_HASH);
		return FALSE;
	}
	return TRUE;
}

/*
 * check the c_hash value in the id_token against the code
 */
apr_byte_t oidc_proto_idtoken_validate_code(request_rec *r, oidc_provider_t *provider, const oidc_jwt_t *jwt,
					    const char *response_type, const char *code) {
	apr_array_header_t *required_for_flows = apr_array_make(r->pool, 2, sizeof(const char *));
	APR_ARRAY_PUSH(required_for_flows, const char *) = OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN;
	APR_ARRAY_PUSH(required_for_flows, const char *) = OIDC_PROTO_RESPONSE_TYPE_CODE_IDTOKEN_TOKEN;
	if (oidc_proto_validate_hash_value(r, provider, jwt, response_type, code, OIDC_CLAIM_C_HASH,
					   required_for_flows) == FALSE) {
		oidc_error(r, "could not validate code against \"%s\" claim value", OIDC_CLAIM_C_HASH);
		return FALSE;
	}
	return TRUE;
}

/*
 * check whether the provided JWT is a valid id_token for the specified "provider"
 */
static apr_byte_t oidc_proto_validate_idtoken(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
					      oidc_jwt_t *jwt, const char *nonce) {

	oidc_debug(r, "enter, jwt.header=\"%s\", jwt.payload=\"%s\", nonce=\"%s\"", jwt->header.value.str,
		   jwt->payload.value.str, nonce);

	/* if a nonce is passed, verify it; otherwise we're doing a ("code") flow where the nonce is optional */
	if ((nonce != NULL) && (oidc_proto_idtoken_validate_nonce(r, cfg, provider, nonce, jwt) == FALSE))
		return FALSE;

	/* validate the ID Token JWT, requiring iss match, and valid exp + iat */
	if (oidc_proto_jwt_validate(
		r, jwt, oidc_cfg_provider_validate_issuer_get(provider) ? oidc_cfg_provider_issuer_get(provider) : NULL,
		TRUE, TRUE, oidc_cfg_provider_idtoken_iat_slack_get(provider)) == FALSE)
		return FALSE;

	/* check if the required-by-spec "sub" claim is present */
	if (jwt->payload.sub == NULL) {
		oidc_error(r, "id_token JSON payload did not contain the required-by-spec \"%s\" string value",
			   OIDC_CLAIM_SUB);
		return FALSE;
	}

	/* verify the "aud" and "azp" values */
	if (oidc_proto_idtoken_validate_aud_and_azp(r, cfg, provider, &jwt->payload) == FALSE)
		return FALSE;

	return TRUE;
}

/*
 * check whether the provided string is a valid id_token and return its parsed contents
 */
apr_byte_t oidc_proto_idtoken_parse(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
				    const char *id_token, const char *nonce, oidc_jwt_t **jwt,
				    apr_byte_t is_code_flow) {

	char *alg = NULL;
	oidc_debug(r, "enter: id_token header=%s", oidc_proto_jwt_header_peek(r, id_token, &alg, NULL, NULL));
	apr_hash_t *decryption_keys = NULL;

	char buf[APR_RFC822_DATE_LEN + 1];
	oidc_jose_error_t err;
	oidc_jwk_t *jwk = NULL;
	if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), oidc_alg2keysize(alg),
					   OIDC_JOSE_ALG_SHA256, TRUE, &jwk) == FALSE)
		return FALSE;

	decryption_keys = oidc_util_key_symmetric_merge(r->pool, oidc_cfg_private_keys_get(cfg), jwk);
	if (oidc_cfg_provider_client_keys_get(provider))
		decryption_keys =
		    oidc_util_key_sets_merge(r->pool, decryption_keys, oidc_cfg_provider_client_keys_get(provider));

	if (oidc_jwt_parse(r->pool, id_token, jwt, decryption_keys, FALSE, &err) == FALSE) {
		oidc_error(r, "oidc_jwt_parse failed: %s", oidc_jose_e2s(r->pool, err));
		oidc_jwt_destroy(*jwt);
		*jwt = NULL;
		oidc_jwk_destroy(jwk);
		return FALSE;
	}

	oidc_jwk_destroy(jwk);
	oidc_debug(r, "successfully parsed (and possibly decrypted) JWT with header=%s, and payload=%s",
		   (*jwt)->header.value.str, (*jwt)->payload.value.str);

	// make a signature-validation exception for the 'code' flow with algorithm "none": the id_token is
	// then obtained over the TLS-protected back-channel, which OpenID Connect Core 1.0 section 3.1.3.7
	// item 6 allows to stand in for validating the signature. A *different* algorithm pinned through
	// OIDCIDTokenSignedResponseAlg (or the client metadata) overrides that exception: honor the pin and
	// reject the unsigned token. A pin of "none" is not such an override - it is the operator, or the
	// OP's own client registration, stating that this OP issues unsigned id_tokens - so it takes the
	// exception too, rather than demanding a signature that by definition cannot verify
	const char *pinned_alg = oidc_cfg_provider_id_token_signed_response_alg_get(provider);
	if ((is_code_flow == FALSE) || (_oidc_strcmp((*jwt)->header.alg, "none") != 0) ||
	    ((pinned_alg != NULL) && (_oidc_strcmp(pinned_alg, "none") != 0))) {

		jwk = NULL;
		if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), 0, NULL, TRUE,
						   &jwk) == FALSE) {
			oidc_jwt_destroy(*jwt);
			*jwt = NULL;
			return FALSE;
		}

		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");
			oidc_jwt_destroy(*jwt);
			*jwt = NULL;
			oidc_jwk_destroy(jwk);
			return FALSE;
		}
		oidc_jwk_destroy(jwk);
	}

	/* this is where the meat is */
	if (oidc_proto_validate_idtoken(r, cfg, provider, *jwt, nonce) == FALSE) {
		oidc_error(r, "id_token payload could not be validated, aborting");
		oidc_jwt_destroy(*jwt);
		*jwt = NULL;
		return FALSE;
	}

	/* log our results */

	apr_time_t expires = oidc_util_apr_time_from_sec((*jwt)->payload.exp);
	apr_rfc822_date(buf, expires);
	oidc_debug(r, "valid id_token for user \"%s\" expires: [%s], in %" APR_TIME_T_FMT " secs from now)",
		   (*jwt)->payload.sub, buf, apr_time_sec(expires) - apr_time_sec(apr_time_now()));

	/* since we've made it so far, we may as well say it is a valid id_token */
	return TRUE;
}
