BaseOAuth2CallbackServlet.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.IOException;
import java.util.Properties;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;

/**
 * Servlet di callback per il flusso OAuth2.
 * 
 * @author Pintori Giuliano (pintori@link.it)
 * @author $Author$
 * @version $Rev$, $Date$
 */
public abstract class BaseOAuth2CallbackServlet extends HttpServlet {

	private static final long serialVersionUID = 1L;

	protected BaseOAuth2CallbackServlet() {
		super();
	}

	@Override
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		engineDoGet(request, response, this.getLog(), this.getLoginProperties());
	}

	private void engineDoGet(HttpServletRequest request, HttpServletResponse response, Logger log, Properties loginProperties) {
		HttpSession session = request.getSession(false);
		try {

			// Log debug sessione callback
			if (session != null) {
				log.debug("[OAuth2Callback] Session ID: {}, Session creation time: {}, Last accessed time: {}",
					session.getId(),
					new java.util.Date(session.getCreationTime()),
					new java.util.Date(session.getLastAccessedTime()));
				String stateInSession = (String) session.getAttribute(OAuth2Costanti.ATTRIBUTE_NAME_OAUTH2_STATE);
				log.debug("[OAuth2Callback] State in session: {}", stateInSession);
			} else {
				log.warn("[OAuth2Callback] Session is NULL - session lost during OAuth2 flow");
			}

			String stateFromRequest = request.getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_STATE);
			log.debug("[OAuth2Callback] State from OAuth2 provider: {}", stateFromRequest);

			// Verifica che la sessione esista prima di validare i parametri
			if (session == null) {
				throw new Oauth2Exception(HttpServletResponse.SC_BAD_REQUEST+"", "Sessione non valida o scaduta. Impossibile completare l'autenticazione OAuth2.");
			}

			String code = validaParametriRichiesta(request, session);

			// Recupera code_verifier dalla sessione se PKCE è abilitato
			String codeVerifier = null;
			if (session != null && OAuth2Utilities.isPkceEnabled(loginProperties)) {
				codeVerifier = (String) session.getAttribute(OAuth2Costanti.ATTRIBUTE_NAME_CODE_VERIFIER);
				// Rimuovi code_verifier dalla sessione dopo l'uso (one-time use)
				session.removeAttribute(OAuth2Costanti.ATTRIBUTE_NAME_CODE_VERIFIER);
			}

			OAuth2Token oAuth2Token = OAuth2Utilities.getToken(log, loginProperties, code, codeVerifier);

			if (oAuth2Token.getReturnCode() != 200) {
				throw new Oauth2Exception( oAuth2Token.getError(), oAuth2Token.getDescription());
			}

			// richiesta dei certificati
			Oauth2BaseResponse jwksResponse = OAuth2Utilities.getCertificati(log, loginProperties); 

			// Verifica errori nella risposta
			if (jwksResponse.getReturnCode() != 200) {
				// Errore nel richiedere i certificati
				throw new Oauth2Exception( jwksResponse.getError(), jwksResponse.getDescription());
			}

			// validazione token (firma + claim configurati)
			boolean valida = OAuth2Utilities.isValidToken(log, loginProperties, jwksResponse, oAuth2Token);

			if (!valida) {
				// Token ricevuto non valido
				OAuth2Utilities.logError(log, OAuth2Costanti.ERROR_MSG_TOKEN_RICEVUTO_NON_VALIDO);
				
				throw new Oauth2Exception(OAuth2Costanti.ERROR_MSG_TOKEN_RICEVUTO_NON_VALIDO, OAuth2Costanti.ERROR_MSG_IMPOSSIBILE_AUTENTICARE_UTENTE + OAuth2Costanti.ERROR_MSG_TOKEN_RICEVUTO_NON_VALIDO);
			}


			Oauth2UserInfo oauth2UserInfo = OAuth2Utilities.getUserInfo(log, loginProperties, oAuth2Token);

			// Verifica errori nella risposta
			if (oauth2UserInfo.getReturnCode() != 200) {
				// Errore nel richiedere userinfo
				throw new Oauth2Exception( oauth2UserInfo.getError(), oauth2UserInfo.getDescription());
			}

			// ricreo la sessione se non presente
			session = request.getSession(true);

			// Log debug sessione dopo autenticazione
			log.debug("[OAuth2Callback] Session ID dopo autenticazione: {}, Access token salvato", session.getId());

			// Salva token e scadenza se vuoi gestire refresh
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ACCESS_TOKEN, oAuth2Token.getAccessToken());
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ID_TOKEN, oAuth2Token.getIdToken());
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ACCESS_TOKEN_OBJ, oAuth2Token);
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_REFRESH_TOKEN, oAuth2Token.getRefreshToken());
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_TOKEN_EXPIRES_AT, oAuth2Token.getExpiresAt());
			session.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_USER_INFO, oauth2UserInfo);

			// 7) Redirect alla home o pagina protetta
			log.debug("[OAuth2Callback] Redirect to console home");
			response.sendRedirect(this.getConsoleHome(request));
		} catch (IOException e) {
			log.error("Errore durante lo scambio del token OAuth2: " + e.getMessage(), e);
			response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
			try {
				response.getWriter().write("Autenticazione Oauth2 non disponibile: si e' verificato un'errore durante lo scambio del token OAuth2: " + e.getMessage());
			} catch (IOException e1) {
				log.error("Errore durante lo scambio del token OAuth2: " + e1.getMessage(), e1);
			}
		} catch (Oauth2Exception e) {
			// Token ricevuto non valido
			OAuth2Utilities.logError(log, e.getErrorCode() + ": " + e.getErrorDetail());

			request.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ERROR_CODE, e.getErrorCode());
			request.setAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ERROR_DETAIL, e.getErrorDetail());
			request.setAttribute(OAuth2Costanti.PARAM_NAME_PRINCIPAL_ERROR_MSG, e.getErrorDetail());
			String redirectUrl = this.getConsoleError(request);
			if(session != null) {
				// Messaggio di errore
				session.setAttribute(OAuth2Costanti.PARAM_NAME_PRINCIPAL_ERROR_MSG, e.getErrorDetail());
				
				String idToken = (String) session.getAttribute(OAuth2Costanti.ATTRIBUTE_NAME_ID_TOKEN);
				String oauth2LogoutUrl = loginProperties.getProperty(OAuth2Costanti.PROP_OAUTH2_LOGOUT_ENDPOINT);
				if(idToken != null) {
					redirectUrl = OAuth2Utilities.creaUrlLogout(idToken, oauth2LogoutUrl, redirectUrl);
				}
			}
			try {
				response.sendRedirect(redirectUrl);
			} catch (IOException e1) {
				log.error("Errore durante esecuzione redirect: " + e1.getMessage(), e1);
			}
		}
	}

	private String validaParametriRichiesta(HttpServletRequest request, HttpSession session) throws Oauth2Exception {
		String code  = request.getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_CODE);
		String state = request.getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_STATE); // opzionale, ma consigliato
		String error = request.getParameter(OAuth2Costanti.PARAM_NAME_OAUTH2_ERROR); // in caso di errore)
		if (error != null) {
			// L'utente ha negato l'accesso o si è verificato un errore
			throw new Oauth2Exception(HttpServletResponse.SC_UNAUTHORIZED+"", "Accesso OAuth2 negato o errore: " + error);
		}

		if (code == null || code.isEmpty()) {
			throw new Oauth2Exception(HttpServletResponse.SC_BAD_REQUEST+"",  "Parametro 'code' mancante.");
		}

		// verifica state
		if (StringUtils.isNotBlank(state)) {
			String stateFromSession = (String) session.getAttribute(OAuth2Costanti.ATTRIBUTE_NAME_OAUTH2_STATE);
			if(StringUtils.isBlank(stateFromSession)) {
				throw new Oauth2Exception(HttpServletResponse.SC_BAD_REQUEST+"", "Parametro 'state' non presente in sessione. La sessione potrebbe essere scaduta durante l'autenticazione OAuth2.");
			}
			if(!stateFromSession.equals(state)) {
				throw new Oauth2Exception(HttpServletResponse.SC_BAD_REQUEST+"", "Parametro 'state' non valido. Possibile tentativo di CSRF o sessione corrotta.");
			}
		}
		return code;
	}

	protected abstract Logger getLog();
	protected abstract Properties getLoginProperties();
	protected abstract String getConsoleHome(HttpServletRequest request); 
	protected abstract String getConsoleError(HttpServletRequest request);
}