/*
 * 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 <limits.h>

#include "handle/handle.h"
#include "metrics.h"
#include "mod_auth_openidc.h"
#include "proto/proto.h"
#include "session.h"
#include "util/util.h"
#include "util/util_cfg.h"

/* JSON object key for the value that holds the refresh token's refresh timestamp */
#define OIDC_REFRESH_TIMESTAMP "ts"

/* Refresh lock lifetime and maximum wait for another caller's result. */
#define OIDC_REFRESH_LOCK_TTL 5

/*
 *  time-to-live (seconds) for the refresh token cache results
 *  during that time other callers trying to execute a refresh grant with the same
 *  refresh token will obtain their results from the cache rather than an actual refresh
 *  request
 */
#define OIDC_REFRESH_CACHE_TTL 30

/* needs to be larger than a few characters for cache compression to work... */
#define OIDC_REFRESH_LOCK_VALUE "needstobelargerthanafewcharacters"

/* needs to be larger than a few characters for cache compression to work... */
#define OIDC_REFRESH_FAILED_LOCK_VALUE "alsoneedstobelargerthanafewcharactersbutdifferent"

/*
 * cache refresh token grant results for a while to avoid (almost) parallel requests
 */
static void oidc_refresh_token_cache_set(request_rec *r, const char *refresh_token, const char *s_access_token,
					 const char *s_token_type, int expires_in, const char *s_id_token,
					 const char *s_refresh_token, apr_time_t *ts) {
	const char *s_json = NULL;

	/* create the JSON representation of the refresh grant results + timestamp */
	oidc_json_t *json = oidc_json_object();
	if (s_access_token)
		oidc_json_object_set_new(json, OIDC_PROTO_ACCESS_TOKEN, oidc_json_string(s_access_token));
	if (s_token_type)
		oidc_json_object_set_new(json, OIDC_PROTO_TOKEN_TYPE, oidc_json_string(s_token_type));
	oidc_json_object_set_new(json, OIDC_PROTO_EXPIRES_IN, oidc_json_integer(expires_in));
	if (s_id_token)
		oidc_json_object_set_new(json, OIDC_PROTO_ID_TOKEN, oidc_json_string(s_id_token));
	if (s_refresh_token)
		oidc_json_object_set_new(json, OIDC_PROTO_REFRESH_TOKEN, oidc_json_string(s_refresh_token));
	*ts = apr_time_now();
	oidc_json_object_set_new(json, OIDC_REFRESH_TIMESTAMP, oidc_json_integer(apr_time_sec(*ts)));

	/* stringify the JSON object and store it in the cache */
	s_json = oidc_json_encode(r->pool, json, OIDC_JSON_COMPACT);
	oidc_debug(r, "caching refresh_token (%s) grant results for %d seconds: %s",
		   oidc_util_mask_value(r, refresh_token), OIDC_REFRESH_CACHE_TTL, oidc_util_mask_value(r, s_json));

	oidc_cache_set_refresh_token(r, refresh_token, s_json,
				     apr_time_now() + apr_time_from_sec(OIDC_REFRESH_CACHE_TTL));

	/* cleanup */
	oidc_json_decref(json);
}

typedef enum {
	OIDC_REFRESH_CACHE_SUCCESS,
	OIDC_REFRESH_CACHE_ERROR,
	OIDC_REFRESH_CACHE_ABORT
} oidc_refresh_token_cache_result_t;
/*
 * parse a cached refresh token grant result (a JSON object string) into the individual output parameters;
 * returns TRUE when the value was a valid JSON object that could be parsed
 */
static apr_byte_t oidc_refresh_token_cache_parse(request_rec *r, const char *s_json, char **s_access_token,
						 char **s_token_type, int *expires_in, char **s_id_token,
						 char **s_refresh_token, apr_time_t *ts) {

	oidc_json_t *json = NULL;
	const oidc_json_t *v = NULL;

	if (oidc_json_decode_object(r, s_json, &json) == FALSE)
		return FALSE;

	/* parse the results from the cache into the output parameters */
	if ((v = oidc_json_object_get(json, OIDC_PROTO_ACCESS_TOKEN)))
		*s_access_token = apr_pstrdup(r->pool, oidc_json_string_value(v));
	if ((v = oidc_json_object_get(json, OIDC_PROTO_TOKEN_TYPE)))
		*s_token_type = apr_pstrdup(r->pool, oidc_json_string_value(v));
	if ((v = oidc_json_object_get(json, OIDC_PROTO_EXPIRES_IN))) {
		/* clamp into int range to match the writer-side guard in oidc_proto_token_response_parse */
		oidc_json_int_t n = oidc_json_integer_value(v);
		if (n > INT_MAX)
			*expires_in = INT_MAX;
		else if (n < INT_MIN)
			*expires_in = INT_MIN;
		else
			*expires_in = (int)n;
	}
	if ((v = oidc_json_object_get(json, OIDC_PROTO_ID_TOKEN)))
		*s_id_token = apr_pstrdup(r->pool, oidc_json_string_value(v));
	if ((v = oidc_json_object_get(json, OIDC_PROTO_REFRESH_TOKEN)))
		*s_refresh_token = apr_pstrdup(r->pool, oidc_json_string_value(v));
	if ((v = oidc_json_object_get(json, OIDC_REFRESH_TIMESTAMP)))
		*ts = apr_time_from_sec(oidc_json_integer_value(v));

	/* cleanup */
	oidc_json_decref(json);

	return TRUE;
}

/*
 * obtain recent refresh token grant results from the cache
 */
static oidc_refresh_token_cache_result_t oidc_refresh_token_cache_get(request_rec *r, const oidc_cfg_t *c,
								      const char *refresh_token, char **s_access_token,
								      char **s_token_type, int *expires_in,
								      char **s_id_token, char **s_refresh_token,
								      apr_time_t *ts) {

	char *s_json = NULL;
	oidc_refresh_token_cache_result_t rv = OIDC_REFRESH_CACHE_ERROR;

	oidc_cache_mutex_lock(r->pool, r->server, oidc_cfg_refresh_mutex_get(c));

	/* see if this token was already refreshed recently or is being refreshed */
	oidc_cache_get_refresh_token(r, refresh_token, &s_json);
	if (s_json == NULL)
		goto no_cache_found;

	/*
	 * Poll only until the lock TTL. Release the global mutex while sleeping, then reacquire it
	 * before reading so the final read and lock claim remain atomic.
	 */
	int retries = OIDC_REFRESH_LOCK_TTL * 2;
	while ((retries-- > 0) && (s_json != NULL) && (_oidc_strcmp(s_json, OIDC_REFRESH_LOCK_VALUE) == 0)) {
		oidc_warn(r, "existing refresh in progress, back off for 0.5s before re-trying the cache");
		oidc_cache_mutex_unlock(r->pool, r->server, oidc_cfg_refresh_mutex_get(c));
		apr_sleep(apr_time_from_msec(500));
		oidc_cache_mutex_lock(r->pool, r->server, oidc_cfg_refresh_mutex_get(c));
		s_json = NULL;
		oidc_cache_get_refresh_token(r, refresh_token, &s_json);
	}

	if (s_json == NULL)
		goto no_cache_found;

	/* check if we have run into an error */
	if (_oidc_strcmp(s_json, OIDC_REFRESH_FAILED_LOCK_VALUE) == 0) {
		oidc_debug(r, "refresh token %s failed to refresh before, do not try to refresh it again but fail",
			   oidc_util_mask_value(r, refresh_token));
		rv = OIDC_REFRESH_CACHE_ABORT;
		goto end;
	}

	/* check if we have run into a timeout */
	if (_oidc_strcmp(s_json, OIDC_REFRESH_LOCK_VALUE) == 0) {
		oidc_warn(r, "timeout waiting for refresh token cache to unlock; attempting our own refresh next "
			     "(in rolling-refresh setups this token may already have been spent by the other caller, "
			     "in which case oidc_refresh_token_grant_obtain_tokens recovers from the cache if our own "
			     "refresh then fails)");
		goto no_cache_found;
	}

	/* we should have valid cache results by now */
	oidc_debug(r, "using cached refresh_token (%s) grant results: %s", oidc_util_mask_value(r, refresh_token),
		   oidc_util_mask_value(r, s_json));

	if (oidc_refresh_token_cache_parse(r, s_json, s_access_token, s_token_type, expires_in, s_id_token,
					   s_refresh_token, ts) == FALSE)
		goto no_cache_found;

	rv = OIDC_REFRESH_CACHE_SUCCESS;

	goto end;

no_cache_found:

	oidc_debug(r, "locking cache and refreshing %s...", oidc_util_mask_value(r, refresh_token));

	/* Best-effort cluster lock; refresh_mutex closes the read/set race only on this machine. */
	oidc_cache_set_refresh_token(r, refresh_token, OIDC_REFRESH_LOCK_VALUE,
				     apr_time_now() + apr_time_from_sec(OIDC_REFRESH_LOCK_TTL));

end:

	oidc_cache_mutex_unlock(r->pool, r->server, oidc_cfg_refresh_mutex_get(c));

	return rv;
}

/*
 * obtain a fresh set of tokens, either by re-using a recent cache entry or
 * by calling the OP token endpoint with the supplied refresh_token; on a
 * remote refresh failure the refresh_token is locked out of future refresh
 * attempts for OIDC_REFRESH_CACHE_TTL seconds
 */
static apr_byte_t oidc_refresh_token_grant_obtain_tokens(request_rec *r, oidc_cfg_t *c, const oidc_provider_t *provider,
							 const char *refresh_token, char **s_id_token,
							 char **s_access_token, char **s_token_type, int *expires_in,
							 char **s_refresh_token, char **s_scope, apr_time_t *ts) {

	/* see if it was refreshed very recently and we can re-use the results from the cache */
	oidc_refresh_token_cache_result_t rv = oidc_refresh_token_cache_get(
	    r, c, refresh_token, s_access_token, s_token_type, expires_in, s_id_token, s_refresh_token, ts);
	if (rv == OIDC_REFRESH_CACHE_SUCCESS)
		return TRUE;
	if (rv == OIDC_REFRESH_CACHE_ABORT)
		/* a prior refresh of the access token failed and we won't try the same again */
		return FALSE;

	oidc_debug(r, "refreshing refresh_token: %s", oidc_util_mask_value(r, refresh_token));

	OIDC_METRICS_TIMING_START(r, c);

	/* refresh the tokens by calling the token endpoint */
	if (oidc_proto_token_refresh_request(r, c, provider, refresh_token, s_id_token, s_access_token, s_token_type,
					     expires_in, s_refresh_token, s_scope) == FALSE) {
		OIDC_METRICS_COUNTER_INC(r, c, OM_PROVIDER_REFRESH_ERROR);
		oidc_error(r, "access_token could not be refreshed");
		/* Another caller may have rotated the token; recheck its cached result before locking it out. */
		char *s_recovered = NULL;
		oidc_cache_get_refresh_token(r, refresh_token, &s_recovered);
		if ((s_recovered != NULL) && (_oidc_strcmp(s_recovered, OIDC_REFRESH_LOCK_VALUE) != 0) &&
		    (_oidc_strcmp(s_recovered, OIDC_REFRESH_FAILED_LOCK_VALUE) != 0) &&
		    (oidc_refresh_token_cache_parse(r, s_recovered, s_access_token, s_token_type, expires_in,
						    s_id_token, s_refresh_token, ts) == TRUE)) {
			oidc_warn(r, "our own refresh grant failed but another caller populated the cache with results "
				     "for the same refresh token; re-using those instead of failing");
			return TRUE;
		}
		/* release the refresh lock and indicate this refresh token should not be refreshed anymore for at least
		 * 30 seconds */
		oidc_cache_set_refresh_token(r, refresh_token, OIDC_REFRESH_FAILED_LOCK_VALUE,
					     apr_time_now() + apr_time_from_sec(OIDC_REFRESH_CACHE_TTL));
		return FALSE;
	}

	OIDC_METRICS_TIMING_ADD(r, c, OM_PROVIDER_REFRESH);

	/* cache the results for other callers */
	oidc_refresh_token_cache_set(r, refresh_token, *s_access_token, *s_token_type, *expires_in, *s_id_token,
				     *s_refresh_token, ts);

	return TRUE;
}

/*
 * OIDC Core 12.2 requires a refreshed ID token to retain the original subject. Skip the check
 * only when the session has no earlier ID-token claims.
 */
static apr_byte_t oidc_refresh_token_grant_id_token_sub_match(request_rec *r, const oidc_session_t *session,
							      const oidc_jwt_t *jwt) {

	const oidc_json_t *claims = oidc_session_get_idtoken_claims(r, session);
	const char *sub =
	    (claims != NULL) ? oidc_json_string_value(oidc_json_object_get(claims, OIDC_CLAIM_SUB)) : NULL;

	if (sub == NULL)
		return TRUE;

	if (_oidc_strcmp(sub, jwt->payload.sub) != 0) {
		oidc_error(r,
			   "the \"%s\" claim (%s) in the refreshed id_token differs from the one established for this "
			   "session (%s): rejecting the refreshed id_token",
			   OIDC_CLAIM_SUB, jwt->payload.sub, sub);
		return FALSE;
	}

	return TRUE;
}

/*
 * apply a refreshed id_token to the session: validate it, store its claims and
 * optionally its serialized form, update the session expiry when no fixed
 * max-duration is configured and return it to the caller
 */
static void oidc_refresh_token_grant_apply_id_token(request_rec *r, oidc_cfg_t *c, oidc_session_t *session,
						    const oidc_provider_t *provider, char *s_id_token,
						    char **new_id_token) {

	oidc_jwt_t *id_token_jwt = NULL;

	/*
	 * Validate as a TLS-protected back-channel ID token. Refreshes have no nonce because the nonce
	 * belongs to the original authentication request.
	 */
	if (oidc_proto_idtoken_parse(r, c, provider, s_id_token, NULL, &id_token_jwt, TRUE) == FALSE) {
		oidc_warn(r, "refreshed id_token could not be validated, retaining the claims obtained earlier");
		return;
	}

	if (oidc_refresh_token_grant_id_token_sub_match(r, session, id_token_jwt) == FALSE) {
		oidc_jwt_destroy(id_token_jwt);
		return;
	}

	/* only store the serialized representation when configured so */
	if (oidc_cfg_store_id_token_get(c))
		oidc_session_set_idtoken(r, session, s_id_token);

	/* store the claims payload in the id_token for later reference */
	oidc_session_set_idtoken_claims(r, session, id_token_jwt->payload.value.json);

	if (oidc_cfg_provider_session_max_duration_get(provider) == 0) {
		/* update the session expiry to match the expiry of the id_token
		 * NB: "exp" is a JSON number, so clamp rather than wrap on a far-future value */
		apr_time_t session_expires = oidc_util_apr_time_from_sec(id_token_jwt->payload.exp);
		oidc_session_set_session_expires(r, session, session_expires);

		/* log message about the updated max session duration */
		oidc_log_session_expires(r, "session max lifetime", session_expires);
	}

	/* see if we need to return it as a parameter */
	if (new_id_token != NULL)
		*new_id_token = s_id_token;

	oidc_jwt_destroy(id_token_jwt);
}

/*
 * execute refresh token grant to refresh the existing access token
 */
apr_byte_t oidc_refresh_token_grant(request_rec *r, oidc_cfg_t *c, oidc_session_t *session,
				    const oidc_provider_t *provider, char **new_access_token,
				    char **new_access_token_type, char **new_id_token) {

	char *s_id_token = NULL;
	char *s_token_type = NULL;
	char *s_access_token = NULL;
	char *s_refresh_token = NULL;
	char *s_scope = NULL;
	int expires_in = -1;
	apr_time_t ts = 0;

	oidc_debug(r, "enter");

	/* get the refresh token that was stored in the session */
	const char *refresh_token = oidc_session_get_refresh_token(r, session);
	if (refresh_token == NULL) {
		oidc_warn(r, "refresh token routine called but no refresh_token found in the session");
		return FALSE;
	}

	if (oidc_refresh_token_grant_obtain_tokens(r, c, provider, refresh_token, &s_id_token, &s_access_token,
						   &s_token_type, &expires_in, &s_refresh_token, &s_scope,
						   &ts) == FALSE)
		return FALSE;

	/* store the new access_token in the session and discard the old one */
	oidc_session_set_access_token(r, session, s_access_token);
	oidc_session_set_access_token_type(r, session, s_token_type);
	oidc_session_set_access_token_expires(r, session, expires_in);

	/* reset the access token refresh timestamp */
	oidc_session_set_access_token_last_refresh(r, session, ts);

	/* see if we need to return it as a parameter */
	if (new_access_token != NULL)
		*new_access_token = s_access_token;
	if (new_access_token_type != NULL)
		*new_access_token_type = s_token_type;

	/* if we have a new refresh token (rolling refresh), store it in the session and overwrite the old one */
	if (s_refresh_token != NULL)
		oidc_session_set_refresh_token(r, session, s_refresh_token);

	/* see if a new scope was returned from the token endpoint */
	if (s_scope != NULL)
		oidc_session_set_scope(r, session, s_scope);

	/* if we have a new id_token, store it in the session and update the session max lifetime if required */
	if (s_id_token != NULL)
		oidc_refresh_token_grant_apply_id_token(r, c, session, provider, s_id_token, new_id_token);

	oidc_debug(r, "replaced refresh_token: %s with %s", oidc_util_mask_value(r, refresh_token),
		   oidc_util_mask_value(r, s_refresh_token));

	return TRUE;
}

/*
 * handle refresh token request
 */
int oidc_refresh_token_request(request_rec *r, oidc_cfg_t *c, oidc_session_t *session) {

	char *return_to = NULL;
	char *r_access_token = NULL;
	const char *error_code = NULL;
	char *error_str = NULL;
	char *error_description = NULL;
	apr_byte_t needs_save = TRUE;
	oidc_provider_t *provider = NULL;

	/* get the command passed to the session management handler */
	oidc_util_url_parameter_get(r, OIDC_REDIRECT_URI_REQUEST_REFRESH, &return_to);
	oidc_util_url_parameter_get(r, OIDC_PROTO_ACCESS_TOKEN, &r_access_token);

	/* check the input parameters */
	if (return_to == NULL) {
		oidc_error(r, "refresh token request handler called with no URL to return to");
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	/* do input validation on the return to parameter value */
	if (oidc_validate_redirect_url(r, c, return_to, OIDC_REDIRECT_URL_SAME_HOST, &error_str, &error_description) ==
	    FALSE) {
		oidc_error(r, "return_to URL validation failed: %s: %s", error_str, error_description);
		return HTTP_INTERNAL_SERVER_ERROR;
	}

	if (r_access_token == NULL) {
		oidc_error(r, "refresh token request handler called with no access_token parameter");
		error_code = "no_access_token";
		goto end;
	}

	const char *s_access_token = oidc_session_get_access_token(r, session);
	if (s_access_token == NULL) {
		oidc_error(r, "no existing access_token found in the session, nothing to refresh");
		error_code = "no_access_token_exists";
		goto end;
	}

	/* compare the access_token parameter used for XSRF protection */
	if (oidc_util_strcmp_const_time(s_access_token, r_access_token) == FALSE) {
		oidc_error(r, "access_token passed in refresh request does not match the one stored in the session");
		error_code = "no_access_token_match";
		goto end;
	}

	/* get a handle to the provider configuration */
	if (oidc_get_provider_from_session(r, c, session, &provider) == FALSE) {
		error_code = "session_corruption";
		goto end;
	}

	/* execute the actual refresh grant */
	if (oidc_refresh_token_grant(r, c, session, provider, NULL, NULL, NULL) == FALSE) {
		oidc_error(r, "access_token could not be refreshed");
		error_code = "refresh_failed";
		goto end;
	}

	/* pass the tokens to the application, possibly updating the expiry */
	oidc_session_pass_tokens(r, c, session, TRUE, &needs_save);

	if (oidc_session_save(r, session, OIDC_SESSION_SAVE_UPDATE) == FALSE) {
		error_code = "error saving session";
		goto end;
	}

end:

	/* pass optional error message to the return URL */
	if (error_code != NULL)
		return_to = apr_psprintf(r->pool, "%s%serror_code=%s", return_to,
					 strchr(return_to, OIDC_CHAR_QUERY) ? OIDC_STR_AMP : OIDC_STR_QUERY,
					 oidc_http_url_encode(r, error_code));

	/* add the redirect location header */
	oidc_http_hdr_out_location_set(r, return_to);

	return HTTP_MOVED_TEMPORARILY;
}

apr_byte_t oidc_refresh_access_token_before_expiry(request_rec *r, oidc_cfg_t *cfg, oidc_session_t *session,
						   int ttl_minimum, apr_byte_t *needs_save) {

	apr_time_t t_expires = -1;
	oidc_provider_t *provider = NULL;

	oidc_debug(r, "ttl_minimum=%d", ttl_minimum);

	if (ttl_minimum < 0)
		return TRUE;

	t_expires = oidc_session_get_access_token_expires(r, session);
	if (t_expires <= 0) {
		oidc_warn(
		    r, "no access token expires_in stored in the session (i.e. returned from the OP in the "
		       "authorization response), so the access token cannot be refreshed based on the TTL requirement");
		return FALSE;
	}

	// NB: this fails early: when no refresh token was returned, an error will be returned on
	//     the first authenticated request, unrelated to the actual access token expiry timestamp
	if (oidc_session_get_refresh_token(r, session) == NULL) {
		oidc_warn(r, "no refresh token stored in the session, so the access token cannot be refreshed based on "
			     "the TTL requirement");
		return FALSE;
	}

	t_expires = t_expires - apr_time_from_sec(ttl_minimum);

	oidc_debug(r, "refresh needed in: %" APR_TIME_T_FMT " seconds", apr_time_sec(t_expires - apr_time_now()));

	if (t_expires > apr_time_now())
		return TRUE;

	if (oidc_get_provider_from_session(r, cfg, session, &provider) == FALSE)
		return FALSE;

	if (oidc_refresh_token_grant(r, cfg, session, provider, NULL, NULL, NULL) == FALSE) {
		oidc_warn(r, "access_token could not be refreshed");
		*needs_save = FALSE;
		return FALSE;
	}

	*needs_save = TRUE;

	return TRUE;
}
