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

/*
 * setup for an endpoint call without authentication
 */
static apr_byte_t oidc_proto_endpoint_auth_none(const char *client_id, apr_table_t *params) {
	apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id);
	return TRUE;
}

/*
 * setup for an endpoint call with OIDC client_secret_basic authentication
 */
static apr_byte_t oidc_proto_endpoint_client_secret_basic(request_rec *r, const char *client_id,
							  const char *client_secret, char **basic_auth_str) {
	oidc_debug(r, "enter");
	if (client_secret == NULL) {
		oidc_error(r, "no client secret is configured");
		return FALSE;
	}
	*basic_auth_str =
	    apr_psprintf(r->pool, "%s:%s", oidc_http_url_encode(r, client_id), oidc_http_url_encode(r, client_secret));

	return TRUE;
}

/*
 * setup for an endpoint call with OIDC client_secret_post authentication
 */
static apr_byte_t oidc_proto_endpoint_client_secret_post(request_rec *r, const char *client_id,
							 const char *client_secret, apr_table_t *params) {
	oidc_debug(r, "enter");
	if (client_secret == NULL) {
		oidc_error(r, "no client secret is configured");
		return FALSE;
	}
	apr_table_set(params, OIDC_PROTO_CLIENT_ID, client_id);
	apr_table_set(params, OIDC_PROTO_CLIENT_SECRET, client_secret);
	return TRUE;
}

/*
 * helper function to create a JWT assertion for endpoint authentication
 */
static apr_byte_t oidc_proto_jwt_create(request_rec *r, const char *client_id, const char *audience, oidc_jwt_t **out) {

	*out = oidc_jwt_new(r->pool, TRUE, TRUE);
	oidc_jwt_t *jwt = *out;

	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_ISS, oidc_json_string(client_id));
	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_SUB, oidc_json_string(client_id));
	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_AUD, oidc_json_string(audience));
	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_JTI, oidc_json_string(oidc_proto_jti_gen(r)));
	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_EXP,
				 oidc_json_integer(apr_time_sec(apr_time_now()) + 60));
	oidc_json_object_set_new(jwt->payload.value.json, OIDC_CLAIM_IAT,
				 oidc_json_integer(apr_time_sec(apr_time_now())));

	return TRUE;
}

/*
 * helper function to add a JWT assertion to the HTTP request as endpoint authentication
 */
static apr_byte_t oidc_proto_jwt_sign_and_add(request_rec *r, apr_table_t *params, oidc_jwt_t *jwt,
					      const oidc_jwk_t *jwk) {
	char *cser = NULL;

	if (oidc_proto_jwt_sign_and_serialize(r, jwk, jwt, &cser) == FALSE)
		return FALSE;

	apr_table_setn(params, OIDC_PROTO_CLIENT_ASSERTION_TYPE, OIDC_PROTO_CLIENT_ASSERTION_TYPE_JWT_BEARER);
	apr_table_set(params, OIDC_PROTO_CLIENT_ASSERTION, cser);

	return TRUE;
}

#define OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG OIDC_JOSE_HDR_ALG_HS256

/*
 * create a JWT assertion signed with the client secret and add it to the HTTP request as endpoint authentication
 */
static apr_byte_t oidc_proto_endpoint_auth_client_secret_jwt(request_rec *r, const char *client_id,
							     const char *client_secret, const char *audience,
							     apr_table_t *params) {
	oidc_jwt_t *jwt = NULL;
	oidc_jose_error_t err;

	oidc_debug(r, "enter");

	if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE)
		return FALSE;

	oidc_jwk_t *jwk = oidc_jwk_create_symmetric_key(r->pool, NULL, (const unsigned char *)client_secret,
							(unsigned int)_oidc_strlen(client_secret), FALSE, &err);
	if (jwk == NULL) {
		oidc_error(r, "parsing of client secret into JWK failed: %s", oidc_jose_e2s(r->pool, err));
		oidc_jwt_destroy(jwt);
		return FALSE;
	}

	jwt->header.alg = apr_pstrdup(r->pool, OIDC_PROTO_JWT_ASSERTION_SYMMETRIC_ALG);

	oidc_proto_jwt_sign_and_add(r, params, jwt, jwk);

	oidc_jwt_destroy(jwt);
	oidc_jwk_destroy(jwk);

	return TRUE;
}

/*
 * helper function that returns the bearer access token as the endpoint authentication method if configured
 */
static apr_byte_t oidc_proto_endpoint_access_token_bearer(request_rec *r, oidc_cfg_t *cfg,
							  const char *bearer_access_token, char **bearer_auth_str) {

	apr_byte_t rv = TRUE;

	if (bearer_access_token != NULL) {
		*bearer_auth_str = apr_psprintf(r->pool, "%s", bearer_access_token);
	} else {
		oidc_error(r, "endpoint auth method set to bearer access token but no token is provided");
		rv = FALSE;
	}

	return rv;
}

/*
 * pick the private signing key (and its accompanying x5t when available) used to sign a private_key_jwt
 * assertion: client-specific keys take precedence over the module-wide configured private keys
 */
static oidc_jwk_t *oidc_proto_endpoint_auth_pick_signing_key(const oidc_cfg_t *cfg,
							     const apr_array_header_t *client_keys, const char **x5t) {
	const oidc_jwk_t *jwk_pub = NULL;
	oidc_jwk_t *jwk = NULL;

	*x5t = NULL;
	if ((client_keys != NULL) && (client_keys->nelts > 0)) {
		jwk = oidc_util_key_list_first(client_keys, -1, OIDC_JOSE_JWK_SIG_STR);
		if (jwk && jwk->x5t)
			*x5t = jwk->x5t;
		return jwk;
	}
	if ((oidc_cfg_private_keys_get(cfg) != NULL) && (oidc_cfg_private_keys_get(cfg)->nelts > 0)) {
		jwk = oidc_util_key_list_first(oidc_cfg_private_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR);
		jwk_pub = oidc_util_key_list_first(oidc_cfg_public_keys_get(cfg), -1, OIDC_JOSE_JWK_SIG_STR);
		if (jwk_pub && jwk_pub->x5t)
			// populate x5t; at least required for Microsoft Entra ID / Azure AD
			*x5t = jwk_pub->x5t;
	}
	return jwk;
}

/*
 * create a JWT assertion signed with the configured private key and add it to the HTTP request as endpoint
 * authentication
 */
static apr_byte_t oidc_proto_endpoint_auth_private_key_jwt(request_rec *r, const oidc_cfg_t *cfg,
							   const char *token_endpoint_auth_alg, const char *client_id,
							   const apr_array_header_t *client_keys, const char *audience,
							   apr_table_t *params) {
	apr_byte_t rv = FALSE;
	oidc_jwt_t *jwt = NULL;
	const oidc_jwk_t *jwk = NULL;
	const char *x5t = NULL;
	const char *alg = NULL;

	oidc_debug(r, "enter");

	if (oidc_proto_jwt_create(r, client_id, audience, &jwt) == FALSE)
		return FALSE;

	jwk = oidc_proto_endpoint_auth_pick_signing_key(cfg, client_keys, &x5t);
	if (jwk == NULL) {
		oidc_error(r, "no private signing keys have been configured to use for private_key_jwt client "
			      "authentication (" OIDCPrivateKeyFiles ")");
		goto end;
	}

	if (x5t != NULL)
		jwt->header.x5t = apr_pstrdup(r->pool, x5t);
	jwt->header.kid = apr_pstrdup(r->pool, jwk->kid);

	if (token_endpoint_auth_alg != NULL) {
		// oidc_proto_jwt_sign_and_add will fail later if no corresponding key was configured
		alg = token_endpoint_auth_alg;
	} else {
		alg = oidc_jwk_default_jws_alg(jwk);
		if (alg == NULL) {
			oidc_error(
			    r, "no valid signing key (RSA or Elliptic Curve) could be found in " OIDCPrivateKeyFiles);
			goto end;
		}
	}
	jwt->header.alg = apr_pstrdup(r->pool, alg);

	oidc_debug(r, "signing private_key_jwt assertion with algorithm: %s", jwt->header.alg);

	// this fails if the wrong type of key  configured for an explicitly specified algorithm
	rv = oidc_proto_jwt_sign_and_add(r, params, jwt, jwk);

end:

	if (jwt)
		oidc_jwt_destroy(jwt);

	return rv;
}

/*
 * Keep this idempotent: retries rebuild the authentication data to obtain a fresh client
 * assertion, so credentials must replace earlier values rather than create duplicates.
 */
apr_byte_t oidc_proto_token_endpoint_auth(request_rec *r, oidc_cfg_t *cfg, const char *token_endpoint_auth,
					  const char *token_endpoint_auth_alg, const char *client_id,
					  const char *client_secret, const apr_array_header_t *client_keys,
					  const char *audience, apr_table_t *params, const char *bearer_access_token,
					  char **basic_auth_str, char **bearer_auth_str) {

	oidc_debug(r, "token_endpoint_auth=%s", token_endpoint_auth);

	if (client_id == NULL) {
		oidc_debug(r, "no client ID set: assume we don't need to authenticate");
		return TRUE;
	}

	// default is client_secret_basic, but only if a client_secret is set,
	// otherwise we are a public client
	if ((token_endpoint_auth == NULL) && (client_secret != NULL))
		token_endpoint_auth = OIDC_PROTO_CLIENT_SECRET_BASIC;

	if ((token_endpoint_auth == NULL) || (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_ENDPOINT_AUTH_NONE) == 0)) {
		oidc_debug(
		    r,
		    "no client secret is configured or the token endpoint auth method was set to \"%s\"; calling the "
		    "token endpoint without client authentication; only public clients are supported",
		    OIDC_PROTO_ENDPOINT_AUTH_NONE);
		return oidc_proto_endpoint_auth_none(client_id, params);
	}

	// RFC 8705: the client authenticates at the TLS layer with its mutual-TLS client
	// certificate; only the client_id is passed in the request body
	if (oidc_cfg_endpoint_auth_is_mtls(token_endpoint_auth)) {
		oidc_debug(r,
			   "\"%s\": authentication occurs at the TLS layer, adding only the client_id to the request",
			   token_endpoint_auth);
		return oidc_proto_endpoint_auth_none(client_id, params);
	}

	// if no client_secret is set and we don't authenticate using private_key_jwt,
	// we can only be a public client since the other methods require a client_secret
	if ((client_secret == NULL) && (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) != 0)) {
		oidc_debug(r, "no client secret set and not using private_key_jwt, assume we are a public client");
		return oidc_proto_endpoint_auth_none(client_id, params);
	}

	if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_BASIC) == 0)
		return oidc_proto_endpoint_client_secret_basic(r, client_id, client_secret, basic_auth_str);

	if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_POST) == 0)
		return oidc_proto_endpoint_client_secret_post(r, client_id, client_secret, params);

	if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_CLIENT_SECRET_JWT) == 0)
		return oidc_proto_endpoint_auth_client_secret_jwt(r, client_id, client_secret, audience, params);

	if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_PRIVATE_KEY_JWT) == 0)
		return oidc_proto_endpoint_auth_private_key_jwt(r, cfg, token_endpoint_auth_alg, client_id, client_keys,
								audience, params);

	if (_oidc_strcmp(token_endpoint_auth, OIDC_PROTO_BEARER_ACCESS_TOKEN) == 0) {
		return oidc_proto_endpoint_access_token_bearer(r, cfg, bearer_access_token, bearer_auth_str);
	}

	oidc_error(r, "uhm, shouldn't be here...");

	return FALSE;
}
