<repo_name>Carnivoroys/TARA-Server<file_sep>/src/test/java/ee/ria/sso/config/ThymeleafSupportTest.java
package ee.ria.sso.config;

import ee.ria.sso.Constants;
import ee.ria.sso.authentication.AuthenticationType;
import ee.ria.sso.flow.ThymeleafSupport;
import ee.ria.sso.service.manager.ManagerService;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.configuration.model.core.CasServerProperties;
import org.apereo.cas.services.OidcRegisteredService;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.webflow.core.collection.SharedAttributeMap;
import org.springframework.webflow.execution.RequestContextHolder;
import org.springframework.webflow.test.MockExternalContext;
import org.springframework.webflow.test.MockRequestContext;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;

@RunWith(MockitoJUnitRunner.class)
public class ThymeleafSupportTest {

    @Mock
    ManagerService managerService;

    @Mock
    CasConfigurationProperties casProperties;

    @Mock
    TaraProperties taraProperties;

    private ThymeleafSupport thymeleafSupport;

    @Before
    public void setUp() {
        thymeleafSupport = new ThymeleafSupport(managerService, casProperties, taraProperties, "paramName");
    }

    @Test
    public void isAuthMethodAllowedShouldReturnTrueWhenMethodsEnabledAndAllowedInSession() {
        Arrays.stream(AuthenticationType.values())
                .forEach(method -> {
                    Mockito.when(taraProperties.isPropertyEnabled(Mockito.eq(method.getPropertyName()+ ".enabled"))).thenReturn(true);
                    setRequestContextWithSessionMap(Collections.singletonMap(
                            Constants.TARA_OIDC_SESSION_AUTH_METHODS, Collections.singletonList(method)
                    ));
                    Assert.assertTrue(this.thymeleafSupport.isAuthMethodAllowed(method));
                });
    }

    @Test
    public void isAuthMethodAllowedShouldReturnFalseWhenMethodsEnabledButNotAllowedInSession() {
        setRequestContextWithSessionMap(Collections.singletonMap(
                Constants.TARA_OIDC_SESSION_AUTH_METHODS, Collections.emptyList()
        ));
        Arrays.stream(AuthenticationType.values())
                .forEach(method -> Assert.assertFalse(this.thymeleafSupport.isAuthMethodAllowed(method)));
    }

    @Test
    public void isAuthMethodAllowedWhenNoAttrSetInSession() {
        Arrays.stream(AuthenticationType.values())
                .forEach(method -> {
                    Mockito.when(taraProperties.isPropertyEnabled(Mockito.eq(method.getPropertyName()+ ".enabled"))).thenReturn(true);
                    setRequestContextWithSessionMap(new HashMap<>());
                    Assert.assertTrue("Method " + method + " should be allowed", this.thymeleafSupport.isAuthMethodAllowed(method));
                });
    }

    @Test
    public void isAuthMethodAllowedShouldReturnFalseWhenMethodsDisabledButAllowedInSession() {
        final ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null,
                casProperties, taraProperties, null);
        Arrays.stream(AuthenticationType.values())
                .forEach(method -> {
                    Mockito.when(taraProperties.isPropertyEnabled(Mockito.eq(method.getPropertyName()+ ".enabled"))).thenReturn(false);
                    setRequestContextWithSessionMap(Collections.singletonMap(
                            Constants.TARA_OIDC_SESSION_AUTH_METHODS, Collections.singletonList(method)
                    ));
                    Assert.assertFalse("Method " + method + " should not be allowed", thymeleafSupport.isAuthMethodAllowed(method));
                });
    }

    @Test
    public void isAuthMethodAllowedShouldReturnFalseWhenMethodsDisabledAndNotAllowedInSession() {
        final ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null,
                casProperties, taraProperties, null);
        setRequestContextWithSessionMap(Collections.singletonMap(
                Constants.TARA_OIDC_SESSION_AUTH_METHODS, Collections.emptyList()
        ));
        Arrays.stream(AuthenticationType.values())
                .forEach(method -> Assert.assertFalse("Method " + method + " should be allowed", thymeleafSupport.isAuthMethodAllowed(method)));
    }

    @Test
    public void getHomeUrlShouldReturnEmptyUrlWhenRedirectUriNotPresentInSession() {
        setRequestContextWithSessionMap(null);
        Assert.assertEquals("#", this.thymeleafSupport.getHomeUrl());
    }

    @Test
    public void getHomeUrlShouldReturnValidHomeUrlWhenValidRedirectUriPresentInSession() {
        OidcRegisteredService oidcRegisteredService = Mockito.mock(OidcRegisteredService.class);
        Mockito.when(oidcRegisteredService.getInformationUrl()).thenReturn("https://client/url");

        ManagerService managerService = Mockito.mock(ManagerService.class);
        Mockito.when(managerService.getServiceByID("https://client/url"))
                .thenReturn(Optional.of(oidcRegisteredService));

        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(managerService, null, null, null);

        setRequestContextWithSessionMap(
                Collections.singletonMap(Constants.TARA_OIDC_SESSION_REDIRECT_URI, "https://client/url")
        );
        Assert.assertEquals("https://client/url", thymeleafSupport.getHomeUrl());
    }

    @Test
    public void getHomeUrlShouldReturnEmptyUrlWhenInvalidRedirectUriPresentInSession() {
        ManagerService managerService = Mockito.mock(ManagerService.class);
        Mockito.when(managerService.getServiceByID("https://client/url"))
                .thenReturn(Optional.empty());

        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(managerService, casProperties, null, null);

        setRequestContextWithSessionMap(Collections.singletonMap(Constants.TARA_OIDC_SESSION_REDIRECT_URI, "https://client/url"));
        Assert.assertEquals("#", thymeleafSupport.getHomeUrl());
    }

    @Test
    public void getLocaleUrlShouldReturn() throws Exception {
        setRequestContextWithSessionMap(new HashMap<>());

        CasServerProperties casServerProperties =  new CasServerProperties();
        casServerProperties.setName("https://example.tara.url");
        Mockito.when(casProperties.getServer()).thenReturn(casServerProperties);

        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, casProperties, null, "somelocaleparam");
        Assert.assertEquals("https://example.tara.url:443?somelocaleparam=et", thymeleafSupport.getLocaleUrl("et"));
    }

    @Test
    public void getBackUrlShouldReturnPac4jRequestedUrlWithSpecifiedLocale() throws Exception {
        setRequestContextWithSessionMap(new HashMap<>());
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, casProperties, null, "somelocaleparam");
        Assert.assertEquals("https://example.tara.ee?somelocaleparam=et", thymeleafSupport.getBackUrl("https://example.tara.ee", Locale.forLanguageTag("et")));
    }

    @Test
    public void getBackUrlShouldReturnHashTagWhenEmpty() throws Exception {
        setRequestContextWithSessionMap(new HashMap<>());
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, casProperties, null, "somelocaleparam");
        Assert.assertEquals("#", thymeleafSupport.getBackUrl("", Locale.forLanguageTag("et")));
        Assert.assertEquals("#", thymeleafSupport.getBackUrl("          ", Locale.forLanguageTag("et")));
        Assert.assertEquals("#", thymeleafSupport.getBackUrl(null, Locale.forLanguageTag("et")));
    }

    @Test
    public void isNotLocaleShouldReturnTrue() {
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, null, null, null);
        Assert.assertTrue(thymeleafSupport.isNotLocale("en", Locale.forLanguageTag("et")));
        Assert.assertTrue(thymeleafSupport.isNotLocale("xxx", Locale.forLanguageTag("et")));
    }

    @Test
    public void isNotLocaleShouldReturnFalse() {
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, null, null, null);
        Assert.assertFalse(thymeleafSupport.isNotLocale("ET", Locale.forLanguageTag("et")));
        Assert.assertFalse(thymeleafSupport.isNotLocale("et", Locale.forLanguageTag("et")));
    }

    @Test
    public void getTestEnvironmentAlertMessageIfAvailableShouldReturnProperyValue() {
        String testMessage = "test123";
        Mockito.when(taraProperties.getTestEnvironmentWarningMessage()).thenReturn(testMessage);
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, null, taraProperties, null);
        Assert.assertEquals(testMessage, thymeleafSupport.getTestEnvironmentAlertMessageIfAvailable());
    }

    @Test
    public void getTestEnvironmentAlertMessageIfAvailableShouldReturnNull() {
        Mockito.when(taraProperties.getTestEnvironmentWarningMessage()).thenReturn(null);
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, null, taraProperties, null);
        Assert.assertEquals(null, thymeleafSupport.getTestEnvironmentAlertMessageIfAvailable());
    }

    @Test
    public void getCurrentRequestIdentifierShouldReturnIdFromRequestAttributes() {
        String uniqueRequestId = UUID.randomUUID().toString();
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setAttribute(Constants.MDC_ATTRIBUTE_REQUEST_ID, uniqueRequestId);
        ThymeleafSupport thymeleafSupport = new ThymeleafSupport(null, null, taraProperties, null);
        Assert.assertEquals(uniqueRequestId, thymeleafSupport.getCurrentRequestIdentifier(request));
    }

    private static void setRequestContextWithSessionMap(final Map<String, Object> sessionMap) {
        mockSpringServletRequestAttributes();
        final MockRequestContext requestContext = new MockRequestContext();
        final MockExternalContext externalContext = new MockExternalContext();
        final SharedAttributeMap<Object> map = externalContext.getSessionMap();

        if (sessionMap != null) sessionMap.forEach(
                (k, v) -> map.put(k, v)
        );

        externalContext.setNativeRequest(new MockHttpServletRequest());
        requestContext.setExternalContext(externalContext);
        RequestContextHolder.setRequestContext(requestContext);
    }

    private static void mockSpringServletRequestAttributes() {
        HttpServletRequest request = new MockHttpServletRequest();
        HttpServletResponse response = new MockHttpServletResponse();
        org.springframework.web.context.request.RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
    }

}
<file_sep>/src/test/resources/dynamic.properties
tara.version=x.y.z<file_sep>/src/main/java/ee/ria/sso/config/mobileid/MobileIDConfiguration.java
package ee.ria.sso.config.mobileid;

import ee.ria.sso.service.mobileid.MobileIDAuthenticationClient;
import ee.ria.sso.service.mobileid.MobileIDAuthenticationService;
import ee.ria.sso.service.mobileid.rest.MobileIDRESTAuthClient;
import ee.ria.sso.statistics.StatisticsHandler;
import ee.sk.mid.MidClient;
import ee.sk.mid.rest.MidLoggingFilter;
import lombok.extern.slf4j.Slf4j;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.client.ClientProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@ConditionalOnProperty("mobile-id.enabled")
@Configuration
@Slf4j
public class MobileIDConfiguration {

    @Autowired
    private StatisticsHandler statisticsHandler;

    @Autowired
    private MobileIDConfigurationProvider configurationProvider;

    @Bean
    public MobileIDAuthenticationClient constructAuthenticationClient() {
        log.info("Initializing REST protocol based authentication client for Mobile-ID REST service");
        return new MobileIDRESTAuthClient(configurationProvider, midClient());
    }

    @Bean
    public MobileIDAuthenticationService mobileIDAuthenticationService() {
        return new MobileIDAuthenticationService(
                statisticsHandler, configurationProvider, constructAuthenticationClient());
    }

    private MidClient midClient() {
        return MidClient.newBuilder()
                .withHostUrl(configurationProvider.getHostUrl())
                .withRelyingPartyUUID(configurationProvider.getRelyingPartyUuid())
                .withRelyingPartyName(configurationProvider.getRelyingPartyName())
                .withNetworkConnectionConfig(clientConfig())
                .withLongPollingTimeoutSeconds(configurationProvider.getSessionStatusSocketOpenDuration())
                .build();
    }

    private ClientConfig clientConfig() {
        ClientConfig clientConfig = new ClientConfig();
        clientConfig.property(ClientProperties.CONNECT_TIMEOUT, configurationProvider.getConnectionTimeout());
        clientConfig.property(ClientProperties.READ_TIMEOUT, configurationProvider.getReadTimeout());
        clientConfig.register(new MidLoggingFilter());
        return clientConfig;
    }
}<file_sep>/src/main/java/ee/ria/sso/oidc/OidcAuthorizeRequestValidationServletFilter.java
package ee.ria.sso.oidc;

import ee.ria.sso.Constants;
import ee.ria.sso.authentication.AuthenticationType;
import ee.ria.sso.authentication.LevelOfAssurance;
import ee.ria.sso.config.TaraProperties;
import ee.ria.sso.config.eidas.EidasConfigurationProvider;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static java.nio.charset.StandardCharsets.UTF_8;

@Slf4j
@AllArgsConstructor
public class OidcAuthorizeRequestValidationServletFilter implements Filter {

    private final OidcAuthorizeRequestValidator oidcAuthorizeRequestValidator;


    private final EidasConfigurationProvider eidasConfigurationProvider;

    private final TaraProperties taraProperties;

    @Override
    public void init(FilterConfig filterConfig) {
        log.debug("Initialize filter: {}", OidcAuthorizeRequestValidationServletFilter.class.getName());
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;

        try {
            this.oidcAuthorizeRequestValidator.validateAuthenticationRequestParameters(request);
            this.saveOidcRequestParametersToSession(request);

            filterChain.doFilter(servletRequest, servletResponse);
        } catch (OidcAuthorizeRequestValidator.InvalidRequestException e) {
            log.error("Invalid OIDC authorization request: " + e.getMessage());
            if (isInvalidClient(e)) {
                throw new IllegalStateException("Invalid authorization request, cannot redirect", e);
            } else {
                response.sendRedirect(getRedirectUrlToRelyingParty(request, e));
            }
        }
    }

    private boolean isInvalidClient(OidcAuthorizeRequestValidator.InvalidRequestException e) {
        return e.getInvalidParameter() == OidcAuthorizeRequestParameter.REDIRECT_URI || e.getInvalidParameter() == OidcAuthorizeRequestParameter.CLIENT_ID;
    }

    private String getRedirectUrlToRelyingParty(HttpServletRequest request, OidcAuthorizeRequestValidator.InvalidRequestException e) {
        String redirectUri = request.getParameter(OidcAuthorizeRequestParameter.REDIRECT_URI.getParameterKey());
        try {
            StringBuilder sb = new StringBuilder();
            sb.append(redirectUri);
            sb.append(redirectUri.contains("?") ? "&" : "?");
            sb.append(String.format("error=%s", URLEncoder.encode(e.getErrorCode(), UTF_8.name())));
            sb.append(String.format("&error_description=%s", URLEncoder.encode(e.getErrorDescription(), UTF_8.name())));
            String state = request.getParameter(OidcAuthorizeRequestParameter.STATE.getParameterKey());
            if (StringUtils.isNotBlank(state)) {
                sb.append(String.format("&state=%s", URLEncoder.encode(state, UTF_8.name())));
            }

            return sb.toString();
        } catch (UnsupportedEncodingException ex) {
            throw new IllegalStateException(ex);
        }
    }

    private void saveOidcRequestParametersToSession(final HttpServletRequest request) {
        final HttpSession session = request.getSession(true);

        String[] scopeElements = getScopeElements(request);
        List<TaraScope> scopes = parseScopes(scopeElements);
        session.setAttribute(Constants.TARA_OIDC_SESSION_SCOPES, scopes);

        if (eidasConfigurationProvider != null) {
            session.setAttribute(Constants.TARA_OIDC_SESSION_SCOPE_EIDAS_COUNTRY, parseScopeEidasCountry(scopeElements).orElse(null));
        }

        session.setAttribute(Constants.TARA_OIDC_SESSION_CLIENT_ID,
                request.getParameter(OidcAuthorizeRequestParameter.CLIENT_ID.getParameterKey())
        );
        session.setAttribute(Constants.TARA_OIDC_SESSION_REDIRECT_URI,
                request.getParameter(OidcAuthorizeRequestParameter.REDIRECT_URI.getParameterKey())
        );

        List<AuthenticationType> authenticationMethodsList = getListOfAllowedAuthenticationMethods(scopes);
        log.debug("List of authentication methods to display on login page: {}", authenticationMethodsList);
        session.setAttribute(Constants.TARA_OIDC_SESSION_AUTH_METHODS,
                authenticationMethodsList
        );

        final String acrValues = request.getParameter(OidcAuthorizeRequestParameter.ACR_VALUES.getParameterKey());
        if (acrValues != null) session.setAttribute(Constants.TARA_OIDC_SESSION_LOA,
                LevelOfAssurance.findByAcrName(acrValues));
    }

    private String[] getScopeElements(HttpServletRequest request) {
        String scopeParameter = request.getParameter(OidcAuthorizeRequestParameter.SCOPE.getParameterKey());
        if (StringUtils.isBlank(scopeParameter)) {
            return new String[0];
        }
        return scopeParameter.split(" ");
    }

    private List<TaraScope> parseScopes(String[] scopeElements) {
        return Arrays.stream(scopeElements)
                .map(scopeElement -> {
                    try {
                        return TaraScope.getScope(scopeElement);
                    } catch (IllegalArgumentException e) {
                        log.warn("Invalid scope value '{}', entry ignored!", scopeElement);
                        return null;
                    }
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    private Optional<TaraScopeValuedAttribute> parseScopeEidasCountry(String[] scopeElements) {
        return Arrays.stream(scopeElements)
                .filter(eidasConfigurationProvider.getAllowedEidasCountryScopeAttributes()::contains)
                .map(this::constructValuedScopeAttribute)
                .filter(Objects::nonNull)
                .findFirst();
    }

    private TaraScopeValuedAttribute constructValuedScopeAttribute(String scopeElement) {
        int lastIndexOf = scopeElement.lastIndexOf(":");
        String scopeAttributeFormalName = scopeElement.substring(0, lastIndexOf);
        String scopeAttributeValue = scopeElement.substring(lastIndexOf + 1);

        TaraScopeValuedAttribute scopeAttribute = null;
        if (StringUtils.isNotBlank(scopeAttributeValue)) {
            scopeAttribute = TaraScopeValuedAttribute.builder()
                    .name(TaraScopeValuedAttributeName.getByFormalName(scopeAttributeFormalName))
                    .value(scopeAttributeValue)
                    .build();
        }
        return scopeAttribute;
    }

    private List<AuthenticationType> getListOfAllowedAuthenticationMethods(final List<TaraScope> scopes) {
        if (scopes.contains(TaraScope.EIDASONLY)) {
            return Arrays.asList(AuthenticationType.eIDAS);
        } else if (isAuthMethodSpecificScopePresent(scopes)) {
            return Arrays.stream(AuthenticationType.values())
                    .filter(e -> scopes.contains(e.getScope()) )
                    .collect(Collectors.toList());
        } else {
            return taraProperties.getDefaultAuthenticationMethods();
        }
    }

    private boolean isAuthMethodSpecificScopePresent(List<TaraScope> scopes) {
        return !Collections.disjoint(scopes, TaraScope.SUPPORTS_AUTHENTICATION_METHOD_SELECTION);
    }

    @Override
    public void destroy() {
        log.debug("Destroy filter: {}", OidcAuthorizeRequestValidationServletFilter.class.getName());
    }

}
