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

/*
 * parse a JWT response from the userinfo endpoint: at this point the response is not a JSON object
 * if the response is an encrypted and/or signed JWT, decrypt/verify it before validating it
 */
static apr_byte_t oidc_proto_userinfo_response_jwt_parse(request_rec *r, oidc_cfg_t *cfg,
							 const oidc_provider_t *provider, char **response,
							 oidc_json_t **claims, char **userinfo_jwt) {
	apr_byte_t rv = FALSE;
	oidc_jose_error_t err;
	oidc_jwk_t *jwk = NULL;
	oidc_jwt_t *jwt = NULL;
	char *alg = NULL;
	char *payload = NULL;
	char *s_jwt_hdr = oidc_proto_jwt_header_peek(r, *response, &alg, NULL, NULL);

	if (s_jwt_hdr == NULL) {
		oidc_error(r, "no JSON/JWT could be parsed from the userinfo endpoint response");
		goto end;
	}

	oidc_debug(r,
		   "enter: JWT header=%s, userinfo_signed_response_alg=%s, userinfo_encrypted_response_alg=%s, "
		   "userinfo_encrypted_response_enc=%s",
		   s_jwt_hdr, oidc_cfg_provider_userinfo_signed_response_alg_get(provider),
		   oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider),
		   oidc_cfg_provider_userinfo_encrypted_response_enc_get(provider));

	if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), oidc_alg2keysize(alg),
					   OIDC_JOSE_ALG_SHA256, TRUE, &jwk) == FALSE)
		goto end;

	if (oidc_cfg_provider_userinfo_encrypted_response_alg_get(provider) != NULL) {
		if (oidc_jwe_decrypt(r->pool, *response,
				     oidc_util_key_symmetric_merge(r->pool, oidc_cfg_private_keys_get(cfg), jwk),
				     &payload, NULL, &err, TRUE) == FALSE) {
			oidc_error(r, "oidc_jwe_decrypt failed: %s", oidc_jose_e2s(r->pool, err));
			goto end;
		}
		oidc_debug(r, "successfully decrypted JWE returned from userinfo endpoint: %s", payload);
		*response = payload;
	}

	if (oidc_cfg_provider_userinfo_signed_response_alg_get(provider) == NULL) {
		oidc_error(r, "no signed userinfo response algorithm configured to verify the JWT returned from the "
			      "userinfo endpoint");
		goto end;
	}

	if (oidc_jwt_parse(r->pool, *response, &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));
		goto end;
	}
	oidc_debug(r, "successfully parsed JWT with header=%s, and payload=%s", jwt->header.value.str,
		   jwt->payload.value.str);

	// discard the encryption key and load the signing key
	oidc_jwk_destroy(jwk);
	jwk = NULL;

	if (oidc_util_key_symmetric_create(r, oidc_cfg_provider_client_secret_get(provider), 0, NULL, TRUE, &jwk) ==
	    FALSE)
		goto end;

	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, NULL, jwk),
				  oidc_cfg_provider_userinfo_signed_response_alg_get(provider)) == FALSE) {

		oidc_error(r, "JWT signature could not be validated, aborting");
		goto end;
	}
	oidc_debug(r, "successfully verified signed JWT returned from userinfo endpoint: %s", jwt->payload.value.str);

	*userinfo_jwt = apr_pstrdup(r->pool, *response);
	*claims = oidc_json_copy(jwt->payload.value.json);
	*response = apr_pstrdup(r->pool, jwt->payload.value.str);

	rv = TRUE;

end:

	if (jwt)
		oidc_jwt_destroy(jwt);
	if (jwk)
		oidc_jwk_destroy(jwk);

	return rv;
}

#define OIDC_COMPOSITE_CLAIM_NAMES "_claim_names"
#define OIDC_COMPOSITE_CLAIM_SOURCES "_claim_sources"
#define OIDC_COMPOSITE_CLAIM_JWT "JWT"
#define OIDC_COMPOSITE_CLAIM_ACCESS_TOKEN OIDC_PROTO_ACCESS_TOKEN
#define OIDC_COMPOSITE_CLAIM_ENDPOINT "endpoint"

/*
 * obtain the JWT payload string for a single aggregated/distributed claim source entry,
 * either directly from the "JWT" member or by fetching it from the configured endpoint
 */
static const char *oidc_proto_userinfo_composite_source_payload(request_rec *r, oidc_cfg_t *cfg,
								const oidc_json_t *value) {
	const oidc_json_t *jwt = oidc_json_object_get(value, OIDC_COMPOSITE_CLAIM_JWT);
	if ((jwt != NULL) && (oidc_json_is_string(jwt)))
		return apr_pstrdup(r->pool, oidc_json_string_value(jwt));

	const char *access_token =
	    oidc_json_string_value(oidc_json_object_get(value, OIDC_COMPOSITE_CLAIM_ACCESS_TOKEN));
	const char *endpoint = oidc_json_string_value(oidc_json_object_get(value, OIDC_COMPOSITE_CLAIM_ENDPOINT));
	if ((access_token == NULL) || (endpoint == NULL))
		return NULL;

	char *s_json = NULL;
	const oidc_provider_t *provider = oidc_cfg_provider_get(cfg);
	oidc_http_get(r, endpoint, NULL, NULL, access_token, NULL, oidc_cfg_provider_ssl_validate_server_get(provider),
		      &s_json, NULL, NULL, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg),
		      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));
	return s_json;
}

/*
 * parse a single aggregated/distributed claim JWT and merge its payload into decoded[key]
 */
static void oidc_proto_userinfo_composite_decode_source(request_rec *r, const oidc_cfg_t *cfg, const char *key,
							const char *s_json, oidc_json_t *decoded) {
	oidc_jose_error_t err;
	oidc_jwt_t *jwt = NULL;

	if (oidc_jwt_parse(r->pool, s_json, &jwt,
			   oidc_util_key_symmetric_merge(r->pool, oidc_cfg_private_keys_get(cfg), NULL), FALSE,
			   &err) == FALSE) {
		oidc_error(r, "could not parse JWT from aggregated claim \"%s\": %s", key, oidc_jose_e2s(r->pool, err));
	} else {
		oidc_json_t *v = oidc_json_object_get(decoded, key);
		if (v == NULL) {
			v = oidc_json_object();
			oidc_json_object_set_new(decoded, key, v);
		}
		oidc_json_merge(r, jwt->payload.value.json, v);
	}
	oidc_jwt_destroy(jwt);
}

/*
 * walk all composite-claim sources, decoding each into the decoded object
 */
static void oidc_proto_userinfo_composite_decode_sources(request_rec *r, oidc_cfg_t *cfg, oidc_json_t *sources,
							 oidc_json_t *decoded) {
	void *iter = oidc_json_object_iter(sources);
	while (iter) {
		const char *key = oidc_json_object_iter_key(iter);
		const oidc_json_t *value = oidc_json_object_iter_value(iter);
		if ((value != NULL) && (oidc_json_is_object(value))) {
			const char *s_json = oidc_proto_userinfo_composite_source_payload(r, cfg, value);
			if ((s_json != NULL) && (_oidc_strcmp(s_json, "") != 0))
				oidc_proto_userinfo_composite_decode_source(r, cfg, key, s_json, decoded);
		}
		iter = oidc_json_object_iter_next(sources, iter);
	}
}

/*
 * resolve each composite-claim name against the decoded sources and copy the value into claims
 */
static void oidc_proto_userinfo_composite_apply_names(request_rec *r, oidc_json_t *names, const oidc_json_t *decoded,
						      oidc_json_t *claims) {
	void *iter = oidc_json_object_iter(names);
	while (iter) {
		const char *key = oidc_json_object_iter_key(iter);
		const char *s_value = oidc_json_string_value(oidc_json_object_iter_value(iter));
		if (s_value == NULL) {
			oidc_warn(r, "no string value found for claim \"%s\"", key);
		} else {
			oidc_debug(r, "processing: %s: %s", key, s_value);
			const oidc_json_t *values = oidc_json_object_get(decoded, s_value);
			if (values == NULL)
				oidc_warn(r, "no values for source \"%s\" found", s_value);
			else
				oidc_json_object_set(claims, key, oidc_json_object_get(values, key));
		}
		iter = oidc_json_object_iter_next(names, iter);
	}
}

/*
 * if the userinfo response contains composite claims then resolve those
 */
static apr_byte_t oidc_proto_userinfo_request_composite_claims(request_rec *r, oidc_cfg_t *cfg, oidc_json_t *claims) {
	oidc_json_t *sources;
	oidc_json_t *names;
	oidc_json_t *decoded;

	oidc_debug(r, "enter");

	names = oidc_json_object_get(claims, OIDC_COMPOSITE_CLAIM_NAMES);
	if ((names == NULL) || (!oidc_json_is_object(names)))
		return FALSE;

	sources = oidc_json_object_get(claims, OIDC_COMPOSITE_CLAIM_SOURCES);
	if ((sources == NULL) || (!oidc_json_is_object(sources))) {
		oidc_debug(r, "%s found, but no %s found", OIDC_COMPOSITE_CLAIM_NAMES, OIDC_COMPOSITE_CLAIM_SOURCES);
		return FALSE;
	}

	decoded = oidc_json_object();

	oidc_proto_userinfo_composite_decode_sources(r, cfg, sources, decoded);
	oidc_proto_userinfo_composite_apply_names(r, names, decoded, claims);

	oidc_json_object_del(claims, OIDC_COMPOSITE_CLAIM_NAMES);
	oidc_json_object_del(claims, OIDC_COMPOSITE_CLAIM_SOURCES);
	oidc_json_decref(decoded);

	return TRUE;
}

/*
 * send the request to the userinfo endpoint
 */
static apr_byte_t oidc_proto_userinfo_endpoint_call(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
						    const char *access_token, const char *dpop, char **response,
						    long *response_code, apr_hash_t *response_hdrs) {

	OIDC_METRICS_TIMING_START(r, cfg);

	/* get the JSON response */
	if (oidc_cfg_provider_userinfo_token_method_get(provider) == OIDC_USER_INFO_TOKEN_METHOD_HEADER) {
		if (oidc_http_get(r, oidc_cfg_provider_userinfo_endpoint_url_get(provider), NULL, NULL, access_token,
				  dpop, oidc_cfg_provider_ssl_validate_server_get(provider), response, response_code,
				  response_hdrs, oidc_cfg_http_timeout_long_get(cfg), oidc_cfg_outgoing_proxy_get(cfg),
				  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, cfg, OM_PROVIDER_USERINFO_ERROR);
			return FALSE;
		}
	} else if (oidc_cfg_provider_userinfo_token_method_get(provider) == OIDC_USER_INFO_TOKEN_METHOD_POST) {
		apr_table_t *params = apr_table_make(r->pool, 4);
		apr_table_setn(params, OIDC_PROTO_ACCESS_TOKEN, access_token);
		if (oidc_http_post_form(r, oidc_cfg_provider_userinfo_endpoint_url_get(provider), params, NULL, NULL,
					dpop, oidc_cfg_provider_ssl_validate_server_get(provider), response,
					response_code, response_hdrs, oidc_cfg_http_timeout_long_get(cfg),
					oidc_cfg_outgoing_proxy_get(cfg), 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, cfg, OM_PROVIDER_USERINFO_ERROR);
			return FALSE;
		}
	} else {
		oidc_error(r, "unsupported userinfo token presentation method: %d",
			   oidc_cfg_provider_userinfo_token_method_get(provider));
		return FALSE;
	}

	OIDC_METRICS_TIMING_ADD(r, cfg, OM_PROVIDER_USERINFO);

	return TRUE;
}

/*
 * allocate the response headers hash used to capture DPoP-related response metadata and
 * create the initial DPoP proof for the userinfo request
 */
static apr_byte_t oidc_proto_userinfo_request_dpop_init(request_rec *r, const oidc_cfg_t *cfg,
							const oidc_provider_t *provider, const char *method,
							const char *access_token, char **dpop,
							apr_hash_t **response_hdrs) {
	*response_hdrs = apr_hash_make(r->pool);
	apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_AUTHORIZATION, APR_HASH_KEY_STRING, "");
	apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_DPOP_NONCE, APR_HASH_KEY_STRING, "");
	apr_hash_set(*response_hdrs, OIDC_HTTP_HDR_CONTENT_TYPE, APR_HASH_KEY_STRING, "");
	return oidc_proto_dpop_create(r, cfg, oidc_cfg_provider_userinfo_endpoint_url_get(provider), method,
				      access_token, NULL, dpop);
}

/*
 * call the userinfo endpoint and decode the response as either a JSON object or a JWT
 */
static apr_byte_t oidc_proto_userinfo_response_resolve(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
						       const char *access_token, const char *dpop,
						       apr_hash_t *response_hdrs, char **s_userinfo,
						       long *response_code, oidc_json_t **userinfo_claims,
						       char **userinfo_jwt) {
	if (oidc_proto_userinfo_endpoint_call(r, cfg, provider, access_token, dpop, s_userinfo, response_code,
					      response_hdrs) == FALSE)
		return FALSE;

	if (oidc_json_decode_object_err(r, *s_userinfo, userinfo_claims, FALSE) == TRUE)
		return TRUE;

	// must be a JWT
	return oidc_proto_userinfo_response_jwt_parse(r, cfg, provider, s_userinfo, userinfo_claims, userinfo_jwt);
}

/*
 * validate that the userinfo response contains a "sub" claim and that it matches the id_token "sub"
 */
static apr_byte_t oidc_proto_userinfo_request_validate_sub(request_rec *r, const char *id_token_sub,
							   const oidc_json_t *userinfo_claims) {
	char *user_info_sub = NULL;
	oidc_jose_get_string(r->pool, userinfo_claims, OIDC_CLAIM_SUB, FALSE, &user_info_sub, NULL);

	oidc_debug(r, "id_token_sub=%s, user_info_sub=%s", id_token_sub, user_info_sub);

	if ((user_info_sub == NULL) && (apr_table_get(r->subprocess_env, "OIDC_NO_USERINFO_SUB") == NULL)) {
		oidc_error(r,
			   "mandatory claim (\"%s\") was not returned from userinfo endpoint "
			   "(https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse)",
			   OIDC_CLAIM_SUB);
		return FALSE;
	}

	if ((id_token_sub != NULL) && (user_info_sub != NULL) && (_oidc_strcmp(id_token_sub, user_info_sub) != 0)) {
		oidc_error(r,
			   "\"%s\" claim (\"%s\") returned from userinfo endpoint does not match the one in "
			   "the id_token (\"%s\")",
			   OIDC_CLAIM_SUB, user_info_sub, id_token_sub);
		return FALSE;
	}

	return TRUE;
}

/*
 * get claims from the OP UserInfo endpoint using the provided access_token
 */
apr_byte_t oidc_proto_userinfo_request(request_rec *r, oidc_cfg_t *cfg, const oidc_provider_t *provider,
				       const char *id_token_sub, const char *access_token,
				       const char *access_token_type, char **s_userinfo, char **userinfo_jwt,
				       oidc_json_t **userinfo_claims, long *response_code) {
	apr_byte_t rv = FALSE;
	char *dpop = NULL;
	apr_hash_t *response_hdrs = NULL;
	const char *method =
	    oidc_cfg_provider_userinfo_token_method_get(provider) == OIDC_USER_INFO_TOKEN_METHOD_POST ? "POST" : "GET";

	oidc_debug(r, "enter, endpoint=%s, access_token=%s, token_type=%s",
		   oidc_cfg_provider_userinfo_endpoint_url_get(provider), oidc_util_mask_value(r, access_token),
		   access_token_type);

	if ((_oidc_strnatcasecmp(access_token_type, OIDC_PROTO_DPOP) == 0) &&
	    (oidc_proto_userinfo_request_dpop_init(r, cfg, provider, method, access_token, &dpop, &response_hdrs) ==
	     FALSE))
		goto end;

	if (oidc_proto_userinfo_response_resolve(r, cfg, provider, access_token, dpop, response_hdrs, s_userinfo,
						 response_code, userinfo_claims, userinfo_jwt) == FALSE)
		goto end;

	/* for plain JSON responses, check for an error and possibly retry with a fresh DPoP nonce */
	if ((*userinfo_jwt == NULL) && (response_hdrs != NULL) &&
	    (oidc_json_check_error(r, *userinfo_claims) == TRUE)) {

		if (oidc_proto_dpop_use_nonce(r, cfg, *userinfo_claims, response_hdrs,
					      oidc_cfg_provider_userinfo_endpoint_url_get(provider), method,
					      access_token, &dpop) == FALSE)
			// a regular error response
			goto end;

		oidc_json_decref(*userinfo_claims);
		*userinfo_claims = NULL;

		if (oidc_proto_userinfo_response_resolve(r, cfg, provider, access_token, dpop, response_hdrs,
							 s_userinfo, response_code, userinfo_claims,
							 userinfo_jwt) == FALSE)
			goto end;

		if (oidc_json_check_error(r, *userinfo_claims) == TRUE)
			goto end;
	}

	if (oidc_proto_userinfo_request_composite_claims(r, cfg, *userinfo_claims) == TRUE)
		*s_userinfo = oidc_json_encode(r->pool, *userinfo_claims, OIDC_JSON_PRESERVE_ORDER | OIDC_JSON_COMPACT);

	if (oidc_proto_userinfo_request_validate_sub(r, id_token_sub, *userinfo_claims) == FALSE)
		goto end;

	rv = TRUE;

end:

	if ((rv == FALSE) && (*userinfo_claims != NULL)) {
		oidc_json_decref(*userinfo_claims);
		*userinfo_claims = NULL;
	}

	return rv;
}
