From 3454a41c5ecbff5e700efc16ee41cb11ec110e66 Mon Sep 17 00:00:00 2001 From: Thomas <> Date: Mon, 6 May 2024 19:03:07 +0200 Subject: feat(core): add optional extended HTTP request validator --- .../idp/auth/AbstractAuthenticationManager.java | 7 ++ .../impl/idp/controller/AbstractController.java | 19 ++++ .../AbstractProcessEngineSignalController.java | 12 ++- .../controller/ProtocolFinalizationController.java | 3 + .../validation/CookieBasedRequestValidator.java | 77 ++++++++++++++ .../impl/idp/validation/IHttpRequestValidator.java | 31 ++++++ .../validation/NothingHttpRequestValidatior.java | 28 +++++ .../messages/eaaf_core_messages.properties | 3 + .../CookieBasedRequestValidatorTest.java | 115 +++++++++++++++++++++ 9 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/CookieBasedRequestValidator.java create mode 100644 eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/IHttpRequestValidator.java create mode 100644 eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/NothingHttpRequestValidatior.java create mode 100644 eaaf_core/src/test/java/at/gv/egiz/eaaf/core/test/impl/idp/validation/CookieBasedRequestValidatorTest.java (limited to 'eaaf_core') diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/AbstractAuthenticationManager.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/AbstractAuthenticationManager.java index 78653cf8..c93f872a 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/AbstractAuthenticationManager.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/auth/AbstractAuthenticationManager.java @@ -50,6 +50,8 @@ import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.idp.auth.modules.ModuleRegistration; import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; import at.gv.egiz.eaaf.core.impl.idp.process.ExecutionContextImpl; +import at.gv.egiz.eaaf.core.impl.idp.validation.IHttpRequestValidator; +import at.gv.egiz.eaaf.core.impl.idp.validation.NothingHttpRequestValidatior; import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils; import jakarta.annotation.PostConstruct; import jakarta.servlet.ServletException; @@ -80,6 +82,8 @@ public abstract class AbstractAuthenticationManager implements IAuthenticationMa protected IRevisionLogger revisionsLogger; @Autowired(required = false) protected ISsoManager ssoManager; + @Autowired(required = false) + protected IHttpRequestValidator httpRequestValidator = new NothingHttpRequestValidatior(); ModuleRegistration moduleRegistration; @@ -146,6 +150,9 @@ public abstract class AbstractAuthenticationManager implements IAuthenticationMa throw new NoPassivAuthenticationException(); } + // inject additional security information + httpRequestValidator.setValidationInfos(httpResp, pendingReq); + // check Single Sign-On functionality if SSOManager is available boolean isValidSsoSession = false; if (ssoManager != null) { diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractController.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractController.java index 41d15743..49aa2b35 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractController.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractController.java @@ -40,11 +40,14 @@ import at.gv.egiz.eaaf.core.api.logging.IRevisionLogger; import at.gv.egiz.eaaf.core.api.storage.ITransactionStorage; import at.gv.egiz.eaaf.core.api.utils.IPendingRequestIdGenerationStrategy; import at.gv.egiz.eaaf.core.exceptions.EaafException; +import at.gv.egiz.eaaf.core.exceptions.EaafSecurityException; import at.gv.egiz.eaaf.core.exceptions.PendingReqIdValidationException; import at.gv.egiz.eaaf.core.exceptions.ProcessExecutionException; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.data.ExceptionContainer; import at.gv.egiz.eaaf.core.impl.data.Pair; +import at.gv.egiz.eaaf.core.impl.idp.validation.IHttpRequestValidator; +import at.gv.egiz.eaaf.core.impl.idp.validation.NothingHttpRequestValidatior; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -75,6 +78,9 @@ public abstract class AbstractController { @Autowired protected IPendingRequestIdGenerationStrategy reqIdGenerationStrategy; + @Autowired(required = false) + protected IHttpRequestValidator httpRequestValidator = new NothingHttpRequestValidatior(); + /** * EAAF framework exception handler. * @@ -145,6 +151,19 @@ public abstract class AbstractController { } + /** + * Extension point to implement additional request validation. + * + * @param httpReq Current HTTP request + * @param pendingReq Current pending-request selected by pendingRequestId + * @throws EaafSecurityException In case of a validation error + */ + protected void extendedRequestValidation(@Nonnull final HttpServletRequest httpReq, + @Nonnull final IRequest pendingReq) throws EaafSecurityException { + httpRequestValidator.validate(httpReq, pendingReq); + + } + protected void handleError(final String errorMessage, final Throwable exceptionThrown, final HttpServletRequest req, final HttpServletResponse resp, IRequest pendingReq) throws IOException, EaafException { diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractProcessEngineSignalController.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractProcessEngineSignalController.java index 46de6167..17d240cb 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractProcessEngineSignalController.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/AbstractProcessEngineSignalController.java @@ -22,8 +22,6 @@ package at.gv.egiz.eaaf.core.impl.idp.controller; import java.io.IOException; import org.apache.commons.text.StringEscapeUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import at.gv.egiz.eaaf.core.api.IRequest; @@ -37,16 +35,17 @@ import at.gv.egiz.eaaf.core.exceptions.PendingReqIdValidationException; import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; /** * Servlet that resumes a suspended process (in case of asynchronous tasks). * * @author tknall + * @author tlenz * */ +@Slf4j public abstract class AbstractProcessEngineSignalController extends AbstractController { - private static final Logger log = - LoggerFactory.getLogger(AbstractProcessEngineSignalController.class); @Autowired(required = true) protected ProcessEngine processEngine; @@ -75,6 +74,9 @@ public abstract class AbstractProcessEngineSignalController extends AbstractCont // change pending-request ID requestStorage.changePendingRequestID(pendingReq); + // extended validation of in-comming HTTP requests + extendedRequestValidation(req, pendingReq); + // process instance is mandatory if (pendingReq.getProcessInstanceId() == null) { throw new EaafIllegalStateException( @@ -99,6 +101,8 @@ public abstract class AbstractProcessEngineSignalController extends AbstractCont } + + /** * Retrieves the current pending-request id from the HttpServletRequest * parameter diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/ProtocolFinalizationController.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/ProtocolFinalizationController.java index a52d2fda..6f8790b2 100644 --- a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/ProtocolFinalizationController.java +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/controller/ProtocolFinalizationController.java @@ -237,6 +237,9 @@ public class ProtocolFinalizationController extends AbstractController { //set MDC variables TransactionIdUtils.setAllLoggingVariables(pendingReq); + // extended validation of in-comming HTTP requests + extendedRequestValidation(req, pendingReq); + //perform protocol finalization steps protAuthService.finalizeAuthentication(req, resp, pendingReq); diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/CookieBasedRequestValidator.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/CookieBasedRequestValidator.java new file mode 100644 index 00000000..98da0c46 --- /dev/null +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/CookieBasedRequestValidator.java @@ -0,0 +1,77 @@ +package at.gv.egiz.eaaf.core.impl.idp.validation; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +import org.apache.commons.lang3.StringUtils; +import org.springframework.web.util.WebUtils; + +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.exceptions.EaafSecurityException; +import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * Validate incoming requests based in HTTP cookies. + */ +@Slf4j +public class CookieBasedRequestValidator implements IHttpRequestValidator { + + public static final String HTTP_COOKIE_SEC = "eaafSession"; + + @Override + public void setValidationInfos(@Nonnull final HttpServletResponse httpResponse, + @Nonnull final IRequest pendingReq) throws EaafSecurityException { + try { + log.debug("Injecting authentication-process HTTP cookie ... "); + String authProcessIdentifier = UUID.randomUUID().toString(); + httpResponse.addCookie(generatePendingRequestIdCookie(authProcessIdentifier, pendingReq)); + pendingReq.setRawDataToTransaction(HTTP_COOKIE_SEC, authProcessIdentifier); + + } catch (MalformedURLException | EaafStorageException e) { + throw new EaafSecurityException("process.81", e); + } + + } + + @Override + public void validate(@Nonnull final HttpServletRequest httpReq, + @Nonnull final IRequest pendingReq) throws EaafSecurityException { + String storedAuthProcessIdentifier = pendingReq.getRawData(HTTP_COOKIE_SEC, String.class); + + if (StringUtils.isNotEmpty(storedAuthProcessIdentifier)) { + Cookie authProcessIdentifier = WebUtils.getCookie(httpReq, HTTP_COOKIE_SEC); + if (storedAuthProcessIdentifier.equals(authProcessIdentifier.getValue())) { + log.trace("Stored authentication-process HTTP cookie matches. Resume process ... "); + + } else { + log.info("Stored authentication-process-Id:{} does not match to Id from HTTP cookie:{}", + storedAuthProcessIdentifier, authProcessIdentifier); + throw new EaafSecurityException("process.80"); + + } + + } else { + log.debug("No stored authentication-process HTTP cookie. Skipping validation ... "); + + } + } + + private Cookie generatePendingRequestIdCookie(final String authProcessIdentifier, final IRequest pendingReq) + throws MalformedURLException { + Cookie cookie = new Cookie( + HTTP_COOKIE_SEC, authProcessIdentifier); + cookie.setHttpOnly(true); + cookie.setSecure(true); + URL url = new URL(pendingReq.getAuthUrlWithOutSlash()); + cookie.setPath(url.getPath()); + return cookie; + + } + +} diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/IHttpRequestValidator.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/IHttpRequestValidator.java new file mode 100644 index 00000000..5c5e0ba1 --- /dev/null +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/IHttpRequestValidator.java @@ -0,0 +1,31 @@ +package at.gv.egiz.eaaf.core.impl.idp.validation; + +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.exceptions.EaafSecurityException; +import jakarta.annotation.Nonnull; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +public interface IHttpRequestValidator { + + /** + * Set validation information to current process. + * + * @param httpResponse Current HTTP response + * @param pendingReq Current pendingRequest + * @throws EaafSecurityException In case of an internal processing error + */ + void setValidationInfos(@Nonnull final HttpServletResponse httpResponse, + @Nonnull final IRequest pendingReq) throws EaafSecurityException; + + /** + * Validate incoming HTTP requests. + * + * @param httpReq Current HTTP request + * @param pendingReq Current pending-request selected by pendingRequestId + * @throws EaafSecurityException In case of a validation error + */ + void validate(HttpServletRequest httpReq, + IRequest pendingReq) throws EaafSecurityException; + +} \ No newline at end of file diff --git a/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/NothingHttpRequestValidatior.java b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/NothingHttpRequestValidatior.java new file mode 100644 index 00000000..073dbbd0 --- /dev/null +++ b/eaaf_core/src/main/java/at/gv/egiz/eaaf/core/impl/idp/validation/NothingHttpRequestValidatior.java @@ -0,0 +1,28 @@ +package at.gv.egiz.eaaf.core.impl.idp.validation; + +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.exceptions.EaafSecurityException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * Blank HTTP request validation with no validation. + */ +@Slf4j +public class NothingHttpRequestValidatior implements IHttpRequestValidator { + + @Override + public void validate(HttpServletRequest httpReq, IRequest pendingReq) + throws EaafSecurityException { + log.trace("Extended HTTP request validation is not implemented ... "); + + } + + @Override + public void setValidationInfos(HttpServletResponse httpResponse, IRequest pendingReq) + throws EaafSecurityException { + + } + +} diff --git a/eaaf_core/src/main/resources/messages/eaaf_core_messages.properties b/eaaf_core/src/main/resources/messages/eaaf_core_messages.properties index c5cb1bb1..a88523f2 100644 --- a/eaaf_core/src/main/resources/messages/eaaf_core_messages.properties +++ b/eaaf_core/src/main/resources/messages/eaaf_core_messages.properties @@ -13,6 +13,9 @@ process.02=Find no applicable authentication process for current state or user-s process.03=Can not resume the authentication process. Reason: {0} process.04=Can not execute authentication process. Problem with an internal state +process.80=Can not continue authentication process, because HTTP security validation failed. +process.81=Can not continue authentication process, because can initialize security context. + process.90=Forward to service-provider not possible, because it's not supported. process.98=Not supported internal state. Reason: {0} diff --git a/eaaf_core/src/test/java/at/gv/egiz/eaaf/core/test/impl/idp/validation/CookieBasedRequestValidatorTest.java b/eaaf_core/src/test/java/at/gv/egiz/eaaf/core/test/impl/idp/validation/CookieBasedRequestValidatorTest.java new file mode 100644 index 00000000..9e02fc91 --- /dev/null +++ b/eaaf_core/src/test/java/at/gv/egiz/eaaf/core/test/impl/idp/validation/CookieBasedRequestValidatorTest.java @@ -0,0 +1,115 @@ +package at.gv.egiz.eaaf.core.test.impl.idp.validation; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.UUID; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.exceptions.EaafSecurityException; +import at.gv.egiz.eaaf.core.impl.idp.auth.dummy.DummyPendingRequest; +import at.gv.egiz.eaaf.core.impl.idp.validation.CookieBasedRequestValidator; +import at.gv.egiz.eaaf.core.test.dummy.DummyAuthConfigMap; +import jakarta.servlet.http.Cookie; +import lombok.SneakyThrows; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration("/SpringTest-context_eaaf_core.xml") +public class CookieBasedRequestValidatorTest { + + DummyAuthConfigMap config = new DummyAuthConfigMap(); + + CookieBasedRequestValidator toCheck = new CookieBasedRequestValidator(); + MockHttpServletRequest httpReq; + IRequest pendingReq; + + /** + * jUnit test initializer. + */ + @Before + @SneakyThrows + public void initialize() { + pendingReq = new DummyPendingRequest(); + + httpReq = new MockHttpServletRequest("POST", "https://localhost/authhandler"); + ((DummyPendingRequest) pendingReq).initialize(httpReq, config); + + } + + @Test + @SneakyThrows + public void setHttpCookie() { + MockHttpServletResponse httpResp = new MockHttpServletResponse(); + toCheck.setValidationInfos(httpResp, pendingReq); + + // validate state + String storedCookie = pendingReq.getRawData(CookieBasedRequestValidator.HTTP_COOKIE_SEC, String.class); + assertNotNull("stored http cookie", storedCookie); + + Cookie cookie = httpResp.getCookie(CookieBasedRequestValidator.HTTP_COOKIE_SEC); + assertNotNull("response http cookie", cookie); + + assertEquals(storedCookie, cookie.getValue(), "cookie value not match"); + + assertTrue("httpOnly", cookie.isHttpOnly()); + assertTrue("secured", cookie.getSecure()); + + assertEquals("", cookie.getPath(), "wrong Context Path"); + + } + + @Test + @SneakyThrows + public void success() { + MockHttpServletResponse httpResp = new MockHttpServletResponse(); + toCheck.setValidationInfos(httpResp, pendingReq); + + // validate state + httpReq.setCookies(httpResp.getCookies()); + toCheck.validate(httpReq, pendingReq); + + } + + @Test + @SneakyThrows + public void notCookieInSession() { + MockHttpServletResponse httpResp = new MockHttpServletResponse(); + toCheck.setValidationInfos(httpResp, pendingReq); + + // validate state + pendingReq.removeRawDataFromTransaction(CookieBasedRequestValidator.HTTP_COOKIE_SEC); + + httpReq.setCookies(httpResp.getCookies()); + toCheck.validate(httpReq, pendingReq); + + } + + @Test + @SneakyThrows + public void wrongCookie() { + MockHttpServletResponse httpResp = new MockHttpServletResponse(); + toCheck.setValidationInfos(httpResp, pendingReq); + + // validate state + + Cookie cookie = httpResp.getCookie(CookieBasedRequestValidator.HTTP_COOKIE_SEC); + cookie.setValue(UUID.randomUUID().toString()); + httpReq.setCookies(cookie); + + EaafSecurityException error = assertThrows(EaafSecurityException.class, + () -> toCheck.validate(httpReq, pendingReq)); + assertEquals("process.80", error.getErrorId(), "wrong ErrorCode"); + + } + +} -- cgit v1.2.3