OAuth2Utilities.java

/*
 * GovWay - A customizable API Gateway 
 * https://govway.org
 * 
 * Copyright (c) 2005-2025 Link.it srl (https://link.it).
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 3, as published by
 * the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package org.openspcoop2.utils.oauth2;

import java.io.Serializable;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.Base64;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.certificate.JWKSet;
import org.openspcoop2.utils.date.DateManager;
import org.openspcoop2.utils.json.JSONUtils;
import org.openspcoop2.utils.random.RandomGenerator;
import org.openspcoop2.utils.security.JOSESerialization;
import org.openspcoop2.utils.security.JWTOptions;
import org.openspcoop2.utils.security.JWTParser;
import org.openspcoop2.utils.security.JsonVerifySignature;
import org.openspcoop2.utils.transport.http.HttpRequest;
import org.openspcoop2.utils.transport.http.HttpRequestMethod;
import org.openspcoop2.utils.transport.http.HttpResponse;
import org.openspcoop2.utils.transport.http.HttpUtilities;
import org.slf4j.Logger;

/**
 * Classe di utilities per le chiamate verso il servizio autenticazione Oauth
 * 
 * @author Pintori Giuliano (pintori@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public class OAuth2Utilities {

	private OAuth2Utilities() { /*static only*/ }

	public static String addFirstParameter(String name, String value) {
		return "?" + getParameter(name, value);
	}
	
	public static String addParameter(String name, String value) {
		return "&" + getParameter(name, value);
	}
	
	public static String getParameter(String name, String value) {
		return name + "=" + URLEncoder.encode(value, StandardCharsets.UTF_8);
	}

	/**
	 * Genera un code_verifier PKCE secondo RFC 7636.
	 * Il code_verifier è una stringa random di alta entropia di lunghezza tra 43 e 128 caratteri,
	 * usando caratteri unreserved [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~"
	 *
	 * @return code_verifier generato (64 caratteri)
	 */
	public static String generateCodeVerifier() {
		RandomGenerator randomGenerator = new RandomGenerator(true);
		byte[] code = new byte[48]; // 48 bytes = 64 caratteri in base64url
		randomGenerator.nextRandomBytes(code);
		return Base64.getUrlEncoder().withoutPadding().encodeToString(code);
	}

	/**
	 * Calcola il code_challenge dal code_verifier usando SHA-256.
	 * code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))
	 *
	 * @param codeVerifier Il code_verifier generato
	 * @return code_challenge calcolato
	 * @throws UtilsException se SHA-256 non è disponibile
	 */
	public static String generateCodeChallenge(String codeVerifier) throws UtilsException {
		return generateCodeChallenge(codeVerifier, OAuth2Costanti.CODE_CHALLENGE_METHOD_S256);
	}

	/**
	 * Calcola il code_challenge dal code_verifier usando il metodo specificato.
	 *
	 * Metodi supportati (RFC 7636):
	 * - S256: code_challenge = BASE64URL(SHA256(ASCII(code_verifier))) [RACCOMANDATO]
	 * - plain: code_challenge = code_verifier
	 *
	 * @param codeVerifier Il code_verifier generato
	 * @param method Il metodo da usare: "S256" o "plain"
	 * @return code_challenge calcolato
	 * @throws UtilsException se il metodo non è supportato o SHA-256 non è disponibile
	 */
	public static String generateCodeChallenge(String codeVerifier, String method) throws UtilsException {
		if (OAuth2Costanti.CODE_CHALLENGE_METHOD_PLAIN.equalsIgnoreCase(method)) {
			// Metodo plain: code_challenge = code_verifier
			return codeVerifier;
		} else if (OAuth2Costanti.CODE_CHALLENGE_METHOD_S256.equalsIgnoreCase(method)) {
			// Metodo S256: code_challenge = BASE64URL(SHA256(code_verifier))
			try {
				MessageDigest digest = MessageDigest.getInstance("SHA-256");
				byte[] hash = digest.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
				return Base64.getUrlEncoder().withoutPadding().encodeToString(hash);
			} catch (NoSuchAlgorithmException e) {
				throw new UtilsException("SHA-256 algorithm not available for PKCE code challenge generation", e);
			}
		} else {
			throw new UtilsException("Unsupported PKCE method: " + method + ". Supported methods are: S256, plain");
		}
	}

	/**
	 * Verifica se PKCE è abilitato nelle properties.
	 *
	 * @param loginProperties Properties di configurazione OAuth2
	 * @return true se PKCE è abilitato, false altrimenti (default: false)
	 */
	public static boolean isPkceEnabled(Properties loginProperties) {
		String pkceEnabled = getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_PKCE_ENABLED);
		return pkceEnabled != null && Boolean.parseBoolean(pkceEnabled);
	}

	/**
	 * Restituisce il metodo PKCE configurato nelle properties.
	 *
	 * @param loginProperties Properties di configurazione OAuth2
	 * @return Il metodo configurato ("S256" o "plain"), default: "S256"
	 */
	public static String getPkceMethod(Properties loginProperties) {
		String method = getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_PKCE_METHOD);
		if (method == null || method.isEmpty()) {
			return OAuth2Costanti.CODE_CHALLENGE_METHOD_S256; // Default: S256 (raccomandato)
		}
		// Normalizza il metodo (case-insensitive)
		if (OAuth2Costanti.CODE_CHALLENGE_METHOD_PLAIN.equalsIgnoreCase(method)) {
			return OAuth2Costanti.CODE_CHALLENGE_METHOD_PLAIN;
		}
		return OAuth2Costanti.CODE_CHALLENGE_METHOD_S256;
	}

	public static String getURLLoginOAuth2(Properties loginProperties, String state) throws UtilsException {
		return getURLLoginOAuth2(loginProperties, state, null);
	}

	public static String getURLLoginOAuth2(Properties loginProperties, String state, String codeChallenge) throws UtilsException {
		return getURLLoginOAuth2(loginProperties, state, codeChallenge, null);
	}

	public static String getURLLoginOAuth2(Properties loginProperties, String state, String codeChallenge, String codeChallengeMethod) throws UtilsException {
		String authorizationEndpoint = checkAndReturnValue(loginProperties, OAuth2Costanti.PROP_OAUTH2_AUTHORIZATION_ENDPOINT);
		String clientId = checkAndReturnValue(loginProperties, OAuth2Costanti.PROP_OAUTH2_CLIENT_ID);
		String callbackUri = checkAndReturnValue(loginProperties, OAuth2Costanti.PROP_OAUTH2_REDIRECT_URL);
		String scope = checkAndReturnValue(loginProperties, OAuth2Costanti.PROP_OAUTH2_SCOPE);

		String url = authorizationEndpoint +
				addFirstParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_RESPONSE_TYPE, "code") +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_REDIRECT_URI, callbackUri) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CLIENT_ID, clientId) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_SCOPE, scope) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_STATE, state);

		// Aggiungi parametri PKCE se code_challenge è fornito
		if (codeChallenge != null && !codeChallenge.isEmpty()) {
			url += addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CODE_CHALLENGE, codeChallenge);
			// Usa il metodo fornito o default S256
			String method = codeChallengeMethod != null ? codeChallengeMethod : OAuth2Costanti.CODE_CHALLENGE_METHOD_S256;
			url += addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CODE_CHALLENGE_METHOD, method);
		}

		return url;
	}
	private static String checkAndReturnValue(Properties loginProperties, String pName) throws UtilsException {
		String value = getProperty(loginProperties, pName);
		if(value==null || StringUtils.isEmpty(value)) {
			throw new UtilsException("Undefined property '"+pName+"'");
		}
		return value;
	}
	private static String getProperty(Properties loginProperties, String pName) {
		String value = loginProperties.getProperty(pName);
		return value!=null ? value.trim() : null;
	}

	private static void injectHttpConfig(HttpRequest req, Properties loginProperties) {
		String hostnameVerifier = getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_HOSTNAME_VERIFIER);
		if (hostnameVerifier != null)
			req.setHostnameVerifier(Boolean.valueOf(hostnameVerifier));
		String trustAllCerts =  getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_TRUST_ALL_CERTS);
		if (trustAllCerts != null)
			req.setTrustAllCerts(Boolean.valueOf(trustAllCerts));
		req.setTrustStorePath(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_TRUSTSTORE));
		req.setTrustStorePassword(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_TRUSTSTORE_PASSWORD));
		req.setTrustStoreType(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_TRUSTSTORE_TYPE));
		req.setCrlPath(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_TRUSTSTORE_CRL));
		
		req.setKeyStorePath(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_KEYSTORE));
		req.setKeyStorePassword(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_KEYSTORE_PASSWORD));
		req.setKeyStoreType(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_KEYSTORE_TYPE));
		req.setKeyAlias(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_KEY_ALIAS));
		req.setKeyPassword(getProperty(loginProperties, OAuth2Costanti.PROP_OAUTH2_HTTPS_KEY_PASSWORD));
	}
	
	public static OAuth2Token getToken(Logger log, Properties loginProperties, String code) {
		return getToken(log, loginProperties, code, null);
	}

	public static OAuth2Token getToken(Logger log, Properties loginProperties, String code, String codeVerifier) {

		String tokenEndpoint = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_TOKEN_ENDPOINT);
		String clientId = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_CLIENT_ID);
		String callbackUri = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_REDIRECT_URL);

		String requestTokenBody =
				getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_GRANT_TYPE, "authorization_code") +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CODE, code) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_REDIRECT_URI, callbackUri) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CLIENT_ID, clientId);

		// Aggiungi code_verifier se PKCE è abilitato
		if (codeVerifier != null && !codeVerifier.isEmpty()) {
			requestTokenBody += addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CODE_VERIFIER, codeVerifier);
		}

		HttpRequest httpRequest = new HttpRequest();

		httpRequest.setMethod(HttpRequestMethod.POST);
		httpRequest.setUrl(tokenEndpoint);
		httpRequest.setContent(requestTokenBody.getBytes()); 
		httpRequest.setContentType(OAuth2Costanti.MEDIA_TYPE_APPLICATION_X_WWW_FORM_URLENCODED);
		httpRequest.setFollowRedirects(false);
		httpRequest.setReadTimeout(getReadTimeout(log, loginProperties));
		httpRequest.setConnectTimeout(getConnectionTimeout(log, loginProperties));
		httpRequest.setDisconnect(false); // Non chiudere la connessione subito, serve per leggere la risposta
		httpRequest.addHeader(OAuth2Costanti.HEADER_NAME_ACCEPT, OAuth2Costanti.MEDIA_TYPE_APPLICATION_JSON);

		injectHttpConfig(httpRequest, loginProperties);
		
		OAuth2Token response = new OAuth2Token();
		try {
			// chiamo servizio token
			HttpResponse httpResponse = HttpUtilities.httpInvoke(httpRequest);

			response.setReturnCode(httpResponse.getResultHTTPOperation());

			String responseBody = new String(httpResponse.getContent());
			Map<String,Serializable> map = JSONUtils.getInstance().convertToMap(log, responseBody, responseBody);

			response.setMap(map);
			response.setRaw(responseBody);

			// Verifica errori nella risposta
			if (httpResponse.getResultHTTPOperation() != 200) {
				String tokenError = (String) map.get(OAuth2Costanti.FIELD_NAME_ERROR);
				String tokenErrorDescription = (String) map.get(OAuth2Costanti.FIELD_NAME_ERROR_DESCRIPTION);
				logError(log, MessageFormat.format("Errore durante l''acquisizione del token: {0}, {1}", tokenError, tokenErrorDescription));

				response.setError(tokenError);
				response.setDescription(tokenErrorDescription);
			}

			// estraggo token
			String accessToken = (String) map.get(OAuth2Costanti.FIELD_NAME_ACCESS_TOKEN);

			JWTParser jwtParser = new JWTParser(accessToken);

			String kid = jwtParser.getHeaderClaim(OAuth2Costanti.FIELD_NAME_KID);
			String alg = jwtParser.getHeaderClaim(OAuth2Costanti.FIELD_NAME_ALGORITHM);

			response.setAccessToken(accessToken);
			response.setKid(kid);
			response.setAlg(alg);

			String expireInS = (String) map.get(OAuth2Costanti.FIELD_NAME_EXPIRES_IN);
			response.setExpiresIn(Long.parseLong(expireInS));
			response.setRefreshToken((String) map.get(OAuth2Costanti.FIELD_NAME_REFRESH_TOKEN));
			response.setScope((String) map.get(OAuth2Costanti.FIELD_NAME_SCOPE));
			response.setTokenType((String) map.get(OAuth2Costanti.FIELD_NAME_TOKEN_TYPE));
			response.setIdToken((String) map.get(OAuth2Costanti.FIELD_NAME_ID_TOKEN));
			if(response.getExpiresIn() != null) {
				response.setExpiresAt(DateManager.getTimeMillis() + response.getExpiresIn() * 1000);
			}

		} catch (UtilsException e) {
			logError(log, "Errore durante l'acquisizione del token: " + e.getMessage(), e);
			response.setReturnCode(500);
			response.setError("Errore durante l'acquisizione del token");
			response.setDescription(e.getMessage());
		}

		return response;
	}

	public static Oauth2BaseResponse getCertificati(Logger log, Properties loginProperties) {
		HttpRequest jwksHttpRequest = new HttpRequest();

		String jwksEndpoint = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_JWKS_ENDPOINT);

		jwksHttpRequest.setMethod(HttpRequestMethod.GET);
		jwksHttpRequest.setUrl(jwksEndpoint);
		jwksHttpRequest.setFollowRedirects(false);
		jwksHttpRequest.setReadTimeout(getReadTimeout(log, loginProperties));
		jwksHttpRequest.setConnectTimeout(getConnectionTimeout(log, loginProperties));
		jwksHttpRequest.setDisconnect(false); // Non chiudere la connessione subito, serve per leggere la risposta

		injectHttpConfig(jwksHttpRequest, loginProperties);
		
		Oauth2BaseResponse response = new Oauth2BaseResponse();
		try {
			HttpResponse jwksHttpResponse = HttpUtilities.httpInvoke(jwksHttpRequest);
			String jwksResponseBody = new String(jwksHttpResponse.getContent());
			Map<String,Serializable> jwksMap = JSONUtils.getInstance().convertToMap(log, jwksResponseBody, jwksResponseBody);

			response.setReturnCode(jwksHttpResponse.getResultHTTPOperation());

			response.setMap(jwksMap);
			response.setRaw(jwksResponseBody);

			// Verifica errori nella risposta
			if (jwksHttpResponse.getResultHTTPOperation() != 200) {
				// Errore nel richiedere i certificati
				String tokenError = (String) jwksMap.get(OAuth2Costanti.FIELD_NAME_ERROR);
				String tokenErrorDescription = (String) jwksMap.get(OAuth2Costanti.FIELD_NAME_ERROR_DESCRIPTION);
				OAuth2Utilities.logError(log, "Errore durante la lettura dei certificati: " + tokenError + ", " + tokenErrorDescription);

				response.setError(tokenError);
				response.setDescription(tokenErrorDescription);
			}
		} catch (UtilsException e) {
			logError(log, "Errore durante la lettura dei certificati: " + e.getMessage(), e);
			response.setReturnCode(500);
			response.setError("Errore durante la lettura dei certificati");
			response.setDescription(e.getMessage());
		}

		return response;
	}

	public static Oauth2UserInfo getUserInfo(Logger log, Properties loginProperties, OAuth2Token oAuth2Token) {
		HttpRequest userInfoHttpRequest = new HttpRequest();

		String userInfoEndpoint = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_USER_INFO_ENDPOINT);

		userInfoHttpRequest.setMethod(HttpRequestMethod.GET);
		userInfoHttpRequest.setUrl(userInfoEndpoint);
		userInfoHttpRequest.setFollowRedirects(false);
		userInfoHttpRequest.setReadTimeout(getReadTimeout(log, loginProperties));
		userInfoHttpRequest.setConnectTimeout(getConnectionTimeout(log, loginProperties));
		userInfoHttpRequest.setDisconnect(false); // Non chiudere la connessione subito, serve per leggere la risposta
		userInfoHttpRequest.addHeader(OAuth2Costanti.HEADER_NAME_ACCEPT, OAuth2Costanti.MEDIA_TYPE_APPLICATION_JSON);
		userInfoHttpRequest.addHeader(OAuth2Costanti.HEADER_NAME_AUTHORIZATION, "Bearer " + oAuth2Token.getAccessToken());

		injectHttpConfig(userInfoHttpRequest, loginProperties);
		
		Oauth2UserInfo response = new Oauth2UserInfo();
		try {
			HttpResponse userInfoHttpResponse = HttpUtilities.httpInvoke(userInfoHttpRequest);

			String userInfoResponseBody = new String(userInfoHttpResponse.getContent());
			Map<String,Serializable> userInfoMap = JSONUtils.getInstance().convertToMap(log, userInfoResponseBody, userInfoResponseBody);

			response.setReturnCode(userInfoHttpResponse.getResultHTTPOperation());

			response.setMap(userInfoMap);
			response.setRaw(userInfoResponseBody);

			// Verifica errori nella risposta
			if (userInfoHttpResponse.getResultHTTPOperation() != 200) {
				// Errore nel richiedere userinfo
				String tokenError = (String) userInfoMap.get(OAuth2Costanti.FIELD_NAME_ERROR);
				String tokenErrorDescription = (String) userInfoMap.get(OAuth2Costanti.FIELD_NAME_ERROR_DESCRIPTION);
				OAuth2Utilities.logError(log, "Errore durante la lettura delle informazioni utente: " + tokenError + ", " + tokenErrorDescription);

				response.setError(tokenError);
				response.setDescription(tokenErrorDescription);
			}

		} catch (UtilsException e) {
			logError(log, "Errore durante la lettura delle informazioni utente: " + e.getMessage(), e);
			response.setReturnCode(500);
			response.setError("Errore durante la lettura delle informazioni utente");
			response.setDescription(e.getMessage());
		}

		return response;
	}

	/**
	 * Valida un token OAuth2 verificando la firma JWT.
	 * Questo metodo esegue solo la verifica della firma crittografica.
	 *
	 * @param log Logger
	 * @param jwksResponse Response contenente i certificati JWKS
	 * @param oAuth2Token Token OAuth2 da validare
	 * @return true se la firma è valida, false altrimenti
	 */
	public static boolean isValidToken(Logger log, Oauth2BaseResponse jwksResponse, OAuth2Token oAuth2Token) {
		// estraggo info dal token
		String accessToken = oAuth2Token.getAccessToken();
		String kid = oAuth2Token.getKid();
		String algoritm = oAuth2Token.getAlg();

		JWTOptions optionsVerify = new JWTOptions(JOSESerialization.COMPACT);

		try {
			JWKSet jwkSet = new JWKSet(jwksResponse.getRaw());

			JsonVerifySignature	jsonVerify = new JsonVerifySignature(jwkSet.getJsonWebKeys(), false, kid, algoritm, optionsVerify);

			return jsonVerify.verify(accessToken);
		} catch (UtilsException e) {
			logError(log, "Errore durante la verifica del token: " + e.getMessage(), e);
			return false;
		}
	}

	/**
	 * Valida un token OAuth2 verificando sia la firma JWT che i claim configurati.
	 * Questo metodo esegue una validazione completa che include:
	 * 1. Verifica della firma crittografica (tramite JWKS)
	 * 2. Validazione dei claim configurati nelle properties (oauth2.claims.*)
	 *
	 * @param log Logger
	 * @param loginProperties Properties contenenti la configurazione OAuth2 (include oauth2.claims.*)
	 * @param jwksResponse Response contenente i certificati JWKS
	 * @param oAuth2Token Token OAuth2 da validare
	 * @return true se sia firma che claim sono validi, false altrimenti
	 */
	public static boolean isValidToken(Logger log, Properties loginProperties,
			Oauth2BaseResponse jwksResponse, OAuth2Token oAuth2Token) {

		// 1. Verifica firma JWT
		boolean signatureValid = isValidToken(log, jwksResponse, oAuth2Token);
		if (!signatureValid) {
			logError(log, "Validazione token fallita: firma JWT non valida");
			return false;
		}

		// 2. Verifica claim configurati (se presenti)
		Oauth2ClaimValidator claimValidator = new Oauth2ClaimValidator(log, loginProperties);
		Oauth2ClaimValidator.ValidationResult claimResult = claimValidator.validate(oAuth2Token.getAccessToken());

		if (!claimResult.isValid()) {
			logError(log, "Validazione token fallita: claim non validi - " + claimResult.getErrorsAsString());
			return false;
		}

		// Token valido sia per firma che per claim
		if (log != null) {
			log.debug("Token validato con successo (firma + claim)");
		}
		return true;
	}

	public static OAuth2Token refreshToken(Logger log, Properties loginProperties, String refreshToken) {

		String tokenEndpoint = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_TOKEN_ENDPOINT);
		String clientId = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_CLIENT_ID);

		String refreshTokenBody =
				getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_GRANT_TYPE, "refresh_token") +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_REFRESH_TOKEN, refreshToken) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CLIENT_ID, clientId);

		HttpRequest httpRequest = new HttpRequest();

		httpRequest.setMethod(HttpRequestMethod.POST);
		httpRequest.setUrl(tokenEndpoint);
		httpRequest.setContent(refreshTokenBody.getBytes()); 
		httpRequest.setContentType(OAuth2Costanti.MEDIA_TYPE_APPLICATION_X_WWW_FORM_URLENCODED);
		httpRequest.setFollowRedirects(false);
		httpRequest.setReadTimeout(getReadTimeout(log, loginProperties));
		httpRequest.setConnectTimeout(getConnectionTimeout(log, loginProperties));
		httpRequest.setDisconnect(false); // Non chiudere la connessione subito, serve per leggere la risposta
		httpRequest.addHeader(OAuth2Costanti.HEADER_NAME_ACCEPT, OAuth2Costanti.MEDIA_TYPE_APPLICATION_JSON);

		injectHttpConfig(httpRequest, loginProperties);
		
		OAuth2Token response = new OAuth2Token();
		try {
			// chiamo servizio token
			HttpResponse httpResponse = HttpUtilities.httpInvoke(httpRequest);

			response.setReturnCode(httpResponse.getResultHTTPOperation());

			String responseBody = new String(httpResponse.getContent());
			Map<String,Serializable> map = JSONUtils.getInstance().convertToMap(log, responseBody, responseBody);

			response.setMap(map);
			response.setRaw(responseBody);

			// Verifica errori nella risposta
			if (httpResponse.getResultHTTPOperation() != 200) {
				String tokenError = (String) map.get(OAuth2Costanti.FIELD_NAME_ERROR);
				String tokenErrorDescription = (String) map.get(OAuth2Costanti.FIELD_NAME_ERROR_DESCRIPTION);
				logError(log, "Errore durante l'acquisizione del token: " + tokenError + ", " + tokenErrorDescription);

				response.setError(tokenError);
				response.setDescription(tokenErrorDescription);
			}

			// estraggo token
			String accessToken = (String) map.get(OAuth2Costanti.FIELD_NAME_ACCESS_TOKEN);
			JWTParser jwtParser = new JWTParser(accessToken);

			String kid = jwtParser.getHeaderClaim(OAuth2Costanti.FIELD_NAME_KID);
			String alg = jwtParser.getHeaderClaim(OAuth2Costanti.FIELD_NAME_ALGORITHM);

			response.setAccessToken(accessToken);
			response.setKid(kid);
			response.setAlg(alg);

			String expireInS = (String) map.get(OAuth2Costanti.FIELD_NAME_EXPIRES_IN);
			response.setExpiresIn(Long.parseLong(expireInS));
			response.setRefreshToken((String) map.get(OAuth2Costanti.FIELD_NAME_REFRESH_TOKEN));
			response.setScope((String) map.get(OAuth2Costanti.FIELD_NAME_SCOPE));
			response.setTokenType((String) map.get(OAuth2Costanti.FIELD_NAME_TOKEN_TYPE));
			response.setIdToken((String) map.get(OAuth2Costanti.FIELD_NAME_ID_TOKEN));
			if(response.getExpiresIn() != null) {
				response.setExpiresAt(DateManager.getTimeMillis() + response.getExpiresIn() * 1000);
			}

		} catch (UtilsException e) {
			logError(log, "Errore durante il refresh del token: " + e.getMessage(), e);
			response.setReturnCode(500);
			response.setError("Errore durante il refresh del token");
			response.setDescription(e.getMessage());
		}

		return response;
	}

	public static void logError(Logger log, String msg) {
		logError(log, msg, null);
	}

	public static void logError(Logger log, String msg, Throwable e) {
		if(e != null) {
			log.error(msg,e);
		} else {
			log.error(msg);
		}
	}

	private static int getReadTimeout(Logger log, Properties loginProperties) {
		String readTimeout = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_READ_TIMEOUT);
		if (readTimeout != null && !readTimeout.isEmpty()) {
			try {
				return Integer.parseInt(readTimeout);
			} catch (NumberFormatException e) {
				logError(log, "Errore nel parsing del read timeout: " + e.getMessage(), e);
			}
		}
		return 15000; // Default read timeout
	}

	private static int getConnectionTimeout(Logger log, Properties loginProperties) {
		String connectTimeout = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_CONNECT_TIMEOUT);
		if (connectTimeout != null && !connectTimeout.isEmpty()) {
			try {
				return Integer.parseInt(connectTimeout);
			} catch (NumberFormatException e) {
				logError(log, "Errore nel parsing del connection timeout: " + e.getMessage(), e);
			}
		}
		return 10000; // Default connection timeout
	}

	public static String creaUrlLogout(String idToken, String oauth2LogoutUrl, String redirPageUrl) {
		return oauth2LogoutUrl +
				addFirstParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_ID_TOKEN_HINT, idToken) +
				addParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_POST_LOGOUT_REDIRECT_URI, redirPageUrl);
	}
}