From 484087a3a61f1b4a57cee8a700c34f1c350172a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 4 Aug 2020 16:27:20 +0200 Subject: [PATCH 1/3] [SYNCOPE-1582] Adding CAS client support --- .../core/logic/wa/WAClientAppLogic.java | 26 ++- .../apache/syncope/fit/sra/CASSRAITCase.java | 157 ++++++++++++++++++ .../syncope/fit/sra/SAML2SRAITCase.java | 2 +- .../test/resources/application-cas.properties | 21 +++ pom.xml | 13 ++ sra/pom.xml | 7 +- .../apache/syncope/sra/SecurityConfig.java | 18 +- .../CASAuthenticationRequestWebFilter.java | 105 ++++++++++++ .../security/cas/CASAuthenticationToken.java | 46 +++++ .../cas/CASAuthenticationWebFilter.java | 105 ++++++++++++ .../security/cas/CASSecurityConfigUtils.java | 98 +++++++++++ .../security/cas/CASServerLogoutHandler.java | 58 +++++++ .../cas/CASServerLogoutSuccessHandler.java | 33 ++++ .../syncope/sra/security/cas/CASUtils.java | 112 +++++++++++++ .../oauth2/OAuth2SecurityConfigUtils.java | 3 +- ...th2SessionRemovalServerLogoutHandler.java} | 17 +- ...ext.java => ServerWebExchangeContext.java} | 20 +-- ...SessionStore.java => WebSessionStore.java} | 20 +-- .../saml2/SAML2AnonymousWebFilter.java | 20 +-- .../security/saml2/SAML2RequestGenerator.java | 16 +- .../saml2/SAML2SecurityConfigUtils.java | 11 +- .../saml2/SAML2ServerLogoutHandler.java | 16 +- ...2WebSsoAuthenticationRequestWebFilter.java | 8 +- .../SAML2WebSsoAuthenticationWebFilter.java | 11 +- .../syncope/sra/session/SessionUtils.java | 39 +++++ .../debug/application-debug.properties | 26 +-- 26 files changed, 922 insertions(+), 86 deletions(-) create mode 100644 fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java create mode 100644 fit/wa-reference/src/test/resources/application-cas.properties create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationRequestWebFilter.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationToken.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationWebFilter.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASSecurityConfigUtils.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutHandler.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutSuccessHandler.java create mode 100644 sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java rename sra/src/main/java/org/apache/syncope/sra/security/{SessionRemovalServerLogoutHandler.java => oauth2/OAuth2SessionRemovalServerLogoutHandler.java} (70%) rename sra/src/main/java/org/apache/syncope/sra/security/pac4j/{ServerHttpContext.java => ServerWebExchangeContext.java} (91%) rename sra/src/main/java/org/apache/syncope/sra/security/pac4j/{ServerHttpSessionStore.java => WebSessionStore.java} (67%) create mode 100644 sra/src/main/java/org/apache/syncope/sra/session/SessionUtils.java diff --git a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAClientAppLogic.java b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAClientAppLogic.java index a1e6765ed80..2d05503b295 100644 --- a/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAClientAppLogic.java +++ b/core/am/logic/src/main/java/org/apache/syncope/core/logic/wa/WAClientAppLogic.java @@ -29,8 +29,10 @@ import org.apache.syncope.common.lib.types.ClientAppType; import org.apache.syncope.common.lib.types.IdRepoEntitlement; import org.apache.syncope.core.persistence.api.dao.NotFoundException; +import org.apache.syncope.core.persistence.api.dao.auth.CASSPDAO; import org.apache.syncope.core.persistence.api.dao.auth.OIDCRPDAO; import org.apache.syncope.core.persistence.api.dao.auth.SAML2SPDAO; +import org.apache.syncope.core.persistence.api.entity.auth.CASSP; import org.apache.syncope.core.persistence.api.entity.auth.OIDCRP; import org.apache.syncope.core.persistence.api.entity.auth.SAML2SP; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +53,9 @@ public class WAClientAppLogic { @Autowired private OIDCRPDAO oidcrpDAO; + @Autowired + private CASSPDAO casspDAO; + @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')") @Transactional(readOnly = true) public List list() { @@ -64,9 +69,14 @@ public List list() { break; case SAML2SP: - default: clientApps.addAll(saml2spDAO.findAll().stream(). map(binder::getWAClientApp).collect(Collectors.toList())); + break; + + case CASSP: + default: + clientApps.addAll(casspDAO.findAll().stream(). + map(binder::getWAClientApp).collect(Collectors.toList())); } }); @@ -91,6 +101,13 @@ private WAClientApp doRead(final Long clientAppId, final ClientAppType type) { } break; + case CASSP: + CASSP cassp = casspDAO.findByClientAppId(clientAppId); + if (cassp != null) { + clientApp = binder.getWAClientApp(cassp); + } + break; + default: } @@ -134,6 +151,13 @@ private WAClientApp doRead(final String name, final ClientAppType type) { } break; + case CASSP: + CASSP cassp = casspDAO.findByName(name); + if (cassp != null) { + clientApp = binder.getWAClientApp(cassp); + } + break; + default: } diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java new file mode 100644 index 00000000000..611d5cce7e4 --- /dev/null +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java @@ -0,0 +1,157 @@ +/* + * 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. + */ +package org.apache.syncope.fit.sra; + +import static org.apache.syncope.fit.sra.AbstractITCase.EN_LANGUAGE; +import static org.apache.syncope.fit.sra.AbstractITCase.WA_ADDRESS; +import static org.apache.syncope.fit.sra.AbstractITCase.extractCASExecution; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.http.Consts; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.impl.client.BasicCookieStore; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.apache.syncope.common.lib.to.client.CASSPTO; +import org.apache.syncope.common.lib.types.ClientAppType; +import org.apache.syncope.common.rest.api.RESTHeaders; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class CASSRAITCase extends AbstractITCase { + + @BeforeAll + public static void startSRA() throws IOException, InterruptedException, TimeoutException { + assumeTrue(CASSRAITCase.class.equals(MethodHandles.lookup().lookupClass())); + + doStartSRA("cas"); + } + + @BeforeAll + public static void clientAppSetup() { + String appName = CASSRAITCase.class.getName(); + CASSPTO clientApp = clientAppService.list(ClientAppType.CASSP).stream(). + filter(app -> appName.equals(app.getName())). + map(CASSPTO.class::cast). + findFirst(). + orElseGet(() -> { + CASSPTO app = new CASSPTO(); + app.setName(appName); + app.setClientAppId(4L); + app.setServiceId("http://localhost:8080/.*"); + + Response response = clientAppService.create(ClientAppType.CASSP, app); + if (response.getStatusInfo().getStatusCode() != Response.Status.CREATED.getStatusCode()) { + fail("Could not create CAS Client App"); + } + + return clientAppService.read( + ClientAppType.CASSP, response.getHeaderString(RESTHeaders.RESOURCE_KEY)); + }); + + clientApp.setAuthPolicy(getAuthPolicy().getKey()); + + clientAppService.update(ClientAppType.CASSP, clientApp); + clientAppService.pushToWA(); + } + + @Test + public void web() throws IOException { + CloseableHttpClient httpclient = HttpClients.createDefault(); + HttpClientContext context = HttpClientContext.create(); + context.setCookieStore(new BasicCookieStore()); + + // 1. public + HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?key1=value1&key2=value2&key2=value3"); + get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + CloseableHttpResponse response = httpclient.execute(get, context); + + ObjectNode headers = checkGetResponse(response, get.getURI().toASCIIString().replace("/public", "")); + assertFalse(headers.has(HttpHeaders.COOKIE)); + + // 2. protected + get = new HttpGet(SRA_ADDRESS + "/protected/get?key1=value1&key2=value2&key2=value3"); + String originalRequestURI = get.getURI().toASCIIString(); + get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + response = httpclient.execute(get, context); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + // 2a. authenticate + String responseBody = EntityUtils.toString(response.getEntity()); + response = authenticateToCas(responseBody, httpclient, context); + + // 2b. WA attribute consent screen + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + responseBody = EntityUtils.toString(response.getEntity()); + String execution = extractCASExecution(responseBody); + + List form = new ArrayList<>(); + form.add(new BasicNameValuePair("_eventId", "confirm")); + form.add(new BasicNameValuePair("execution", execution)); + form.add(new BasicNameValuePair("option", "1")); + form.add(new BasicNameValuePair("reminder", "30")); + form.add(new BasicNameValuePair("reminderTimeUnit", "days")); + + HttpPost post = new HttpPost(WA_ADDRESS + "/login"); + post.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + post.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + post.setEntity(new UrlEncodedFormEntity(form, Consts.UTF_8)); + response = httpclient.execute(post, context); + } + assertEquals(HttpStatus.SC_MOVED_TEMPORARILY, response.getStatusLine().getStatusCode()); + + // 2c. finally get requested content + get = new HttpGet(response.getFirstHeader(HttpHeaders.LOCATION).getValue()); + get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); + get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + response = httpclient.execute(get, context); + + headers = checkGetResponse(response, originalRequestURI.replace("/protected", "")); + assertFalse(headers.get(HttpHeaders.COOKIE).asText().isBlank()); + + // 3. logout + get = new HttpGet(SRA_ADDRESS + "/protected/logout"); + get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); + response = httpclient.execute(get, context); + + checkLogout(response); + } +} diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java index 762f2b47929..7361e4f04a2 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java @@ -35,7 +35,6 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; -import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Triple; import org.apache.commons.text.StringEscapeUtils; import org.apache.http.Consts; @@ -223,6 +222,7 @@ public void web() throws IOException { // 2d. post SAML response responseBody = EntityUtils.toString(response.getEntity()); + System.out.println("XXXXXXXXXXXXXXXXXXX3\n" + responseBody); parsed = parseSAMLResponseForm(responseBody); post = new HttpPost(parsed.getLeft()); diff --git a/fit/wa-reference/src/test/resources/application-cas.properties b/fit/wa-reference/src/test/resources/application-cas.properties new file mode 100644 index 00000000000..acc6237937b --- /dev/null +++ b/fit/wa-reference/src/test/resources/application-cas.properties @@ -0,0 +1,21 @@ +# 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. +am.type=CAS +am.cas.server.name=http://localhost:80 +am.cas.url.prefix=http://localhost:9080/syncope-wa/ + +global.postLogout=http://localhost:8080/logout diff --git a/pom.xml b/pom.xml index 6d819b42689..27c29855f6c 100644 --- a/pom.xml +++ b/pom.xml @@ -450,6 +450,7 @@ under the License. 4.0.3 6.3.0-SNAPSHOT + 3.6.1 1.4.200 @@ -1374,6 +1375,12 @@ under the License. org.pac4j pac4j-saml ${pac4j.version} + + + org.apache.velocity + velocity + + org.pac4j @@ -1643,6 +1650,12 @@ under the License. cas-server-webapp-config ${cas.version} + + + org.jasig.cas.client + cas-client-core + ${cas-client.version} + diff --git a/sra/pom.xml b/sra/pom.xml index 51a114eecde..183b170ca05 100644 --- a/sra/pom.xml +++ b/sra/pom.xml @@ -82,7 +82,12 @@ under the License. pac4j-saml - + + org.jasig.cas.client + cas-client-core + + + org.springframework.session spring-session-core diff --git a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java index 89646f3ac20..9d9fda1ddb4 100644 --- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java +++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java @@ -27,10 +27,12 @@ import org.apache.syncope.sra.security.LogoutRouteMatcher; import org.apache.syncope.sra.security.oauth2.OAuth2SecurityConfigUtils; import org.apache.syncope.sra.security.PublicRouteMatcher; +import org.apache.syncope.sra.security.cas.CASSecurityConfigUtils; import org.apache.syncope.sra.security.saml2.SAML2BindingType; import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint; import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils; import org.apache.syncope.sra.security.saml2.SAML2WebSsoAuthenticationWebFilter; +import org.jasig.cas.client.Protocol; import org.pac4j.core.http.callback.NoParameterCallbackUrlResolver; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.config.SAML2Configuration; @@ -266,10 +268,24 @@ public SecurityWebFilterChain routesSecurityFilterChain( case SAML2: SAML2Client saml2Client = saml2Client(); SAML2SecurityConfigUtils.forLogin(http, saml2Client, publicRouteMatcher); - SAML2SecurityConfigUtils.forLogout(builder, saml2Client, logoutRouteMatcher, ctx); + SAML2SecurityConfigUtils.forLogout(builder, saml2Client, cacheManager, logoutRouteMatcher, ctx); break; case CAS: + CASSecurityConfigUtils.forLogin( + http, + env.getProperty("am.cas.server.name"), + Protocol.CAS3, + env.getProperty("am.cas.url.prefix"), + publicRouteMatcher); + CASSecurityConfigUtils.forLogout( + builder, + cacheManager, + env.getProperty("am.cas.url.prefix"), + logoutRouteMatcher, + ctx); + break; + default: } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationRequestWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationRequestWebFilter.java new file mode 100644 index 00000000000..7d741fdb573 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationRequestWebFilter.java @@ -0,0 +1,105 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import java.net.URI; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.sra.security.PublicRouteMatcher; +import org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy; +import org.apache.syncope.sra.session.SessionUtils; +import org.jasig.cas.client.Protocol; +import org.jasig.cas.client.util.CommonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +public class CASAuthenticationRequestWebFilter implements WebFilter { + + private static final Logger LOG = LoggerFactory.getLogger(CASAuthenticationRequestWebFilter.class); + + private final ServerWebExchangeMatcher matcher; + + /** + * The name of the server. Should be in the following format: {protocol}:{hostName}:{port}. + * Standard ports can be excluded. + */ + private final String serverName; + + private final Protocol protocol; + + /** + * The URL to the CAS Server login. + */ + private final String casServerLoginUrl; + + private ServerRedirectStrategy authenticationRedirectStrategy = new DoNothingIfCommittedServerRedirectStrategy(); + + public CASAuthenticationRequestWebFilter( + final PublicRouteMatcher publicRouteMatcher, + final String serverName, + final Protocol protocol, + final String casServerUrlPrefix) { + + this.matcher = ServerWebExchangeMatchers.matchers( + publicRouteMatcher, + CASUtils.ticketAvailable(protocol), + SessionUtils.authInSession()); + this.serverName = serverName; + this.protocol = protocol; + this.casServerLoginUrl = StringUtils.appendIfMissing(casServerUrlPrefix, "/") + "login"; + } + + public void setAuthenticationRedirectStrategy(final ServerRedirectStrategy authenticationRedirectStrategy) { + this.authenticationRedirectStrategy = authenticationRedirectStrategy; + } + + @Override + public Mono filter(final ServerWebExchange exchange, final WebFilterChain chain) { + return matcher.matches(exchange). + filter(matchResult -> !matchResult.isMatch()). + switchIfEmpty(chain.filter(exchange).then(Mono.empty())). + flatMap(r -> exchange.getSession()). + flatMap(session -> { + session.getAttributes(). + put(SessionUtils.INITIAL_REQUEST_URI, exchange.getRequest().getURI()); + + LOG.debug("no ticket and no assertion found"); + + String serviceUrl = CASUtils.constructServiceUrl(exchange, this.serverName, this.protocol); + LOG.debug("Constructed service url: {}", serviceUrl); + + String urlToRedirectTo = CommonUtils.constructRedirectUrl( + this.casServerLoginUrl, + this.protocol.getServiceParameterName(), + serviceUrl, + false, + false, + null); + LOG.debug("redirecting to \"{}\"", urlToRedirectTo); + + return authenticationRedirectStrategy.sendRedirect(exchange, URI.create(urlToRedirectTo)); + }); + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationToken.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationToken.java new file mode 100644 index 00000000000..bf5d23cd8bb --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationToken.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import org.apache.commons.lang3.StringUtils; +import org.jasig.cas.client.validation.Assertion; +import org.springframework.security.authentication.AbstractAuthenticationToken; + +public class CASAuthenticationToken extends AbstractAuthenticationToken { + + private static final long serialVersionUID = 6333776590644298469L; + + private final Assertion assertion; + + public CASAuthenticationToken(final Assertion assertion) { + super(null); + this.assertion = assertion; + this.setAuthenticated(true); + } + + @Override + public Object getCredentials() { + return StringUtils.EMPTY; + } + + @Override + public Assertion getPrincipal() { + return assertion; + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationWebFilter.java new file mode 100644 index 00000000000..224c1b2e4b7 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASAuthenticationWebFilter.java @@ -0,0 +1,105 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import java.net.URI; +import org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy; +import org.apache.syncope.sra.session.SessionUtils; +import org.jasig.cas.client.Protocol; +import org.jasig.cas.client.validation.Assertion; +import org.jasig.cas.client.validation.TicketValidationException; +import org.jasig.cas.client.validation.TicketValidator; +import org.jasig.cas.client.validation.json.Cas30JsonServiceTicketValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter; +import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler; +import org.springframework.security.web.server.util.matcher.AndServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.NegatedServerWebExchangeMatcher; +import reactor.core.publisher.Mono; + +public class CASAuthenticationWebFilter extends AuthenticationWebFilter { + + private static final Logger LOG = LoggerFactory.getLogger(CASAuthenticationWebFilter.class); + + private final String serverName; + + private final Protocol protocol; + + private final TicketValidator ticketValidator; + + public CASAuthenticationWebFilter( + final ReactiveAuthenticationManager authenticationManager, + final String serverName, + final Protocol protocol, + final String casServerUrlPrefix) { + + super(authenticationManager); + + this.serverName = serverName; + this.protocol = protocol; + this.ticketValidator = new Cas30JsonServiceTicketValidator(casServerUrlPrefix); + + setRequiresAuthenticationMatcher(new AndServerWebExchangeMatcher( + CASUtils.ticketAvailable(protocol), + new NegatedServerWebExchangeMatcher(SessionUtils.authInSession()))); + + setServerAuthenticationConverter(validateAssertion()); + + setAuthenticationSuccessHandler(redirectToInitialRequestURI()); + } + + private ServerAuthenticationConverter validateAssertion() { + return exchange -> CASUtils.retrieveTicketFromRequest(exchange, this.protocol). + flatMap(ticket -> { + try { + Assertion assertion = this.ticketValidator.validate( + ticket, + CASUtils.constructServiceUrl(exchange, this.serverName, this.protocol)); + return Mono.just(new CASAuthenticationToken(assertion)); + } catch (TicketValidationException e) { + LOG.error("Could not validate {}", ticket, e); + throw new BadCredentialsException("Could not validate " + ticket); + } + }); + } + + private ServerAuthenticationSuccessHandler redirectToInitialRequestURI() { + return new ServerAuthenticationSuccessHandler() { + + private final ServerRedirectStrategy redirectStrategy = new DoNothingIfCommittedServerRedirectStrategy(); + + @Override + public Mono onAuthenticationSuccess( + final WebFilterExchange webFilterExchange, final Authentication authentication) { + + return webFilterExchange.getExchange().getSession(). + flatMap(session -> this.redirectStrategy.sendRedirect( + webFilterExchange.getExchange(), + session.getRequiredAttribute(SessionUtils.INITIAL_REQUEST_URI))); + } + }; + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASSecurityConfigUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASSecurityConfigUtils.java new file mode 100644 index 00000000000..1c159ba8c1e --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASSecurityConfigUtils.java @@ -0,0 +1,98 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import org.apache.syncope.sra.ApplicationContextUtils; +import org.apache.syncope.sra.security.LogoutRouteMatcher; +import org.apache.syncope.sra.security.PublicRouteMatcher; +import org.jasig.cas.client.Protocol; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter; +import org.springframework.security.web.server.authentication.logout.LogoutWebFilter; +import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; +import reactor.core.publisher.Mono; + +public final class CASSecurityConfigUtils { + + private static final Logger LOG = LoggerFactory.getLogger(CASSecurityConfigUtils.class); + + private static ReactiveAuthenticationManager authenticationManager() { + return authentication -> Mono.just(authentication).filter(Authentication::isAuthenticated); + } + + public static void forLogin( + final ServerHttpSecurity http, + final String serverName, + final Protocol protocol, + final String casServerUrlPrefix, + final PublicRouteMatcher publicRouteMatcher) { + + ReactiveAuthenticationManager authenticationManager = authenticationManager(); + + CASAuthenticationRequestWebFilter authRequestFilter = new CASAuthenticationRequestWebFilter( + publicRouteMatcher, + serverName, + protocol, + casServerUrlPrefix); + http.addFilterAt(authRequestFilter, SecurityWebFiltersOrder.HTTP_BASIC); + + AuthenticationWebFilter authenticationFilter = new CASAuthenticationWebFilter( + authenticationManager, + serverName, protocol, + casServerUrlPrefix); + authenticationFilter.setAuthenticationFailureHandler((exchange, ex) -> Mono.error(ex)); + authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository()); + http.addFilterAt(authenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION); + } + + public static void forLogout( + final ServerHttpSecurity.AuthorizeExchangeSpec builder, + final CacheManager cacheManager, + final String casServerUrlPrefix, + final LogoutRouteMatcher logoutRouteMatcher, + final ConfigurableApplicationContext ctx) { + + LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); + logoutWebFilter.setRequiresLogoutMatcher(logoutRouteMatcher); + + logoutWebFilter.setLogoutHandler(new CASServerLogoutHandler(cacheManager, casServerUrlPrefix)); + + try { + CASServerLogoutSuccessHandler handler = ApplicationContextUtils.getOrCreateBean(ctx, + CASServerLogoutSuccessHandler.class.getName(), + CASServerLogoutSuccessHandler.class); + logoutWebFilter.setLogoutSuccessHandler(handler); + } catch (ClassNotFoundException e) { + LOG.error("While creating instance of {}", CASServerLogoutSuccessHandler.class.getName(), e); + } + + builder.and().addFilterAt(logoutWebFilter, SecurityWebFiltersOrder.LOGOUT); + } + + private CASSecurityConfigUtils() { + // private constructor for static utility class + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutHandler.java new file mode 100644 index 00000000000..3c62df027e3 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutHandler.java @@ -0,0 +1,58 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import java.net.URI; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.sra.SessionConfig; +import org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy; +import org.springframework.cache.CacheManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.ServerRedirectStrategy; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; +import reactor.core.publisher.Mono; + +public class CASServerLogoutHandler implements ServerLogoutHandler { + + private final ServerRedirectStrategy redirectStrategy = new DoNothingIfCommittedServerRedirectStrategy(); + + private final CacheManager cacheManager; + + /** + * The URL to the CAS Server logout. + */ + private final String casServerLogoutUrl; + + public CASServerLogoutHandler(final CacheManager cacheManager, final String casServerUrlPrefix) { + this.cacheManager = cacheManager; + this.casServerLogoutUrl = StringUtils.appendIfMissing(casServerUrlPrefix, "/") + "logout"; + } + + @Override + public Mono logout(final WebFilterExchange exchange, final Authentication authentication) { + return exchange.getExchange().getSession(). + flatMap(session -> { + cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); + + return session.invalidate().then( + redirectStrategy.sendRedirect(exchange.getExchange(), URI.create(this.casServerLogoutUrl))); + }).onErrorResume(Mono::error); + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutSuccessHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutSuccessHandler.java new file mode 100644 index 00000000000..9d5c4b644c3 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASServerLogoutSuccessHandler.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import org.apache.syncope.sra.security.AbstractServerLogoutSuccessHandler; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.server.WebFilterExchange; +import reactor.core.publisher.Mono; + +public class CASServerLogoutSuccessHandler extends AbstractServerLogoutSuccessHandler { + + @Override + public Mono onLogoutSuccess(final WebFilterExchange exchange, final Authentication authentication) { + return Mono.just(authentication). + flatMap(auth -> redirectStrategy.sendRedirect(exchange.getExchange(), getPostLogout(exchange))); + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java new file mode 100644 index 00000000000..5e2da27e107 --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java @@ -0,0 +1,112 @@ +/* + * 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. + */ +package org.apache.syncope.sra.security.cas; + +import java.util.List; +import org.jasig.cas.client.Protocol; +import org.jasig.cas.client.util.URIBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; +import reactor.core.publisher.Mono; + +public final class CASUtils { + + private static final Logger LOG = LoggerFactory.getLogger(CASUtils.class); + + public static Mono safeGetParameter(final ServerWebExchange exchange, final String parameter) { + if (exchange.getRequest().getMethod() == HttpMethod.POST) { + LOG.debug( + "safeGetParameter called on a POST ServerHttpRequest for Restricted Parameters. " + + "Cannot complete check safely. " + + "Reverting to standard behavior for this Parameter"); + return exchange.getFormData(). + flatMap(form -> Mono.justOrEmpty(form.getFirst(parameter))); + } + return Mono.justOrEmpty(exchange.getRequest().getQueryParams().getFirst(parameter)); + } + + public static Mono retrieveTicketFromRequest(final ServerWebExchange exchange, final Protocol protocol) { + return safeGetParameter(exchange, protocol.getArtifactParameterName()); + } + + public static ServerWebExchangeMatcher ticketAvailable(final Protocol protocol) { + return exchange -> CASUtils.retrieveTicketFromRequest(exchange, protocol). + flatMap(ticket -> ServerWebExchangeMatcher.MatchResult.match()). + switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); + } + + public static String constructServiceUrl( + final ServerWebExchange exchange, + final String serverName, + final Protocol protocol) { + + UriComponents requestURI = UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build(); + + URIBuilder originalRequestUrl = new URIBuilder(requestURI.toUriString(), false); + originalRequestUrl.setParameters(requestURI.getQuery()); + + URIBuilder builder; + if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) { + String scheme = exchange.getRequest().getSslInfo() == null ? "http://" : "https://"; + builder = new URIBuilder(scheme + serverName, false); + } else { + builder = new URIBuilder(serverName, false); + } + + builder.setPort(requestURI.getPort()); + + builder.setEncodedPath(builder.getEncodedPath() + requestURI.getPath()); + + List serviceParameterNames = List.of(protocol.getServiceParameterName().split(",")); + if (!serviceParameterNames.isEmpty() && !originalRequestUrl.getQueryParams().isEmpty()) { + originalRequestUrl.getQueryParams().forEach(pair -> { + String name = pair.getName(); + if (!name.equals(protocol.getArtifactParameterName()) && !serviceParameterNames.contains(name)) { + if (name.contains("&") || name.contains("=")) { + URIBuilder encodedParamBuilder = new URIBuilder(); + encodedParamBuilder.setParameters(name); + encodedParamBuilder.getQueryParams().forEach(pair2 -> { + String name2 = pair2.getName(); + if (!name2.equals(protocol.getArtifactParameterName()) + && !serviceParameterNames.contains(name2)) { + + builder.addParameter(name2, pair2.getValue()); + } + }); + } else { + builder.addParameter(name, pair.getValue()); + } + } + }); + } + + String result = builder.toString(); + LOG.debug("serviceUrl generated: {}", result); + return result; + } + + private CASUtils() { + // private constructor for static utility class + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java index cf6bbb410db..6a92551e02c 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SecurityConfigUtils.java @@ -22,7 +22,6 @@ import org.apache.syncope.sra.ApplicationContextUtils; import org.apache.syncope.sra.SecurityConfig.AMType; import org.apache.syncope.sra.security.LogoutRouteMatcher; -import org.apache.syncope.sra.security.SessionRemovalServerLogoutHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; @@ -122,7 +121,7 @@ public static void forLogout( LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); logoutWebFilter.setRequiresLogoutMatcher(logoutRouteMatcher); - logoutWebFilter.setLogoutHandler(new SessionRemovalServerLogoutHandler(cacheManager)); + logoutWebFilter.setLogoutHandler(new OAuth2SessionRemovalServerLogoutHandler(cacheManager)); if (AMType.OIDC == amType) { try { diff --git a/sra/src/main/java/org/apache/syncope/sra/security/SessionRemovalServerLogoutHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SessionRemovalServerLogoutHandler.java similarity index 70% rename from sra/src/main/java/org/apache/syncope/sra/security/SessionRemovalServerLogoutHandler.java rename to sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SessionRemovalServerLogoutHandler.java index 0c2558639a3..9cf3d0ec6e6 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/SessionRemovalServerLogoutHandler.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/oauth2/OAuth2SessionRemovalServerLogoutHandler.java @@ -16,33 +16,28 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.syncope.sra.security; +package org.apache.syncope.sra.security.oauth2; import org.apache.syncope.sra.SessionConfig; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.cache.CacheManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import reactor.core.publisher.Mono; -public class SessionRemovalServerLogoutHandler implements ServerLogoutHandler { - - private static final Logger EVENTS = LoggerFactory.getLogger("events"); +public class OAuth2SessionRemovalServerLogoutHandler implements ServerLogoutHandler { private final CacheManager cacheManager; - public SessionRemovalServerLogoutHandler(final CacheManager cacheManager) { + public OAuth2SessionRemovalServerLogoutHandler(final CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override public Mono logout(final WebFilterExchange exchange, final Authentication authentication) { - return exchange.getExchange().getSession().doOnNext(session -> { - session.invalidate(); - EVENTS.debug("Invalidate session {}", (authentication == null) ? null : authentication.getPrincipal()); + return exchange.getExchange().getSession().flatMap(session -> { cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); - }).flatMap(session -> Mono.empty()); + return session.invalidate(); + }); } } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java similarity index 91% rename from sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java rename to sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java index 1e0b3abab26..bf22e69e6c7 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpContext.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java @@ -36,11 +36,11 @@ import org.springframework.web.server.WebSession; import org.springframework.web.util.UriComponentsBuilder; -public class ServerHttpContext implements WebContext { +public class ServerWebExchangeContext implements WebContext { private final ServerWebExchange exchange; - private ServerHttpSessionStore sessionStore; + private WebSessionStore sessionStore; private MultiValueMap form; @@ -52,8 +52,8 @@ public class ServerHttpContext implements WebContext { * @param exchange the current exchange * @param webSession the current web session */ - public ServerHttpContext(final ServerWebExchange exchange, final WebSession webSession) { - this(exchange, new ServerHttpSessionStore(webSession)); + public ServerWebExchangeContext(final ServerWebExchange exchange, final WebSession webSession) { + this(exchange, new WebSessionStore(webSession)); } /** @@ -62,9 +62,9 @@ public ServerHttpContext(final ServerWebExchange exchange, final WebSession webS * @param exchange the current exchange * @param sessionStore the session store to use */ - public ServerHttpContext( + public ServerWebExchangeContext( final ServerWebExchange exchange, - final ServerHttpSessionStore sessionStore) { + final WebSessionStore sessionStore) { CommonHelper.assertNotNull("exchange", exchange); CommonHelper.assertNotNull("sessionStore", sessionStore); @@ -72,12 +72,12 @@ public ServerHttpContext( this.sessionStore = sessionStore; } - public ServerHttpSessionStore getNativeSessionStore() { + public WebSessionStore getNativeSessionStore() { return this.sessionStore; } @Override - public SessionStore getSessionStore() { + public SessionStore getSessionStore() { return this.sessionStore; } @@ -103,7 +103,7 @@ public Optional getRequestParameter(final String name) { return Optional.empty(); } - public ServerHttpContext setForm(final MultiValueMap form) { + public ServerWebExchangeContext setForm(final MultiValueMap form) { this.form = form; return this; } @@ -214,7 +214,7 @@ public String getPath() { return exchange.getRequest().getPath().value(); } - public ServerHttpContext setBody(final String body) { + public ServerWebExchangeContext setBody(final String body) { this.body = body; return this; } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/WebSessionStore.java similarity index 67% rename from sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java rename to sra/src/main/java/org/apache/syncope/sra/security/pac4j/WebSessionStore.java index 622a92c5c45..c74336c6924 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerHttpSessionStore.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/WebSessionStore.java @@ -22,47 +22,47 @@ import org.pac4j.core.context.session.SessionStore; import org.springframework.web.server.WebSession; -public class ServerHttpSessionStore implements SessionStore { +public class WebSessionStore implements SessionStore { private final WebSession webSession; - public ServerHttpSessionStore(final WebSession webSession) { + public WebSessionStore(final WebSession webSession) { this.webSession = webSession; } @Override - public String getOrCreateSessionId(final ServerHttpContext context) { + public String getOrCreateSessionId(final ServerWebExchangeContext context) { return this.webSession.getId(); } @Override - public Optional get(final ServerHttpContext context, final String key) { + public Optional get(final ServerWebExchangeContext context, final String key) { return Optional.ofNullable(this.webSession.getAttribute(key)); } @Override - public void set(final ServerHttpContext context, final String key, final Object value) { + public void set(final ServerWebExchangeContext context, final String key, final Object value) { } @Override - public boolean destroySession(final ServerHttpContext context) { + public boolean destroySession(final ServerWebExchangeContext context) { return false; } @Override - public Optional getTrackableSession(final ServerHttpContext context) { + public Optional getTrackableSession(final ServerWebExchangeContext context) { return Optional.ofNullable(this.webSession); } @Override - public Optional> buildFromTrackableSession( - final ServerHttpContext context, final Object trackableSession) { + public Optional> buildFromTrackableSession( + final ServerWebExchangeContext context, final Object trackableSession) { return Optional.empty(); } @Override - public boolean renewSession(final ServerHttpContext context) { + public boolean renewSession(final ServerWebExchangeContext context) { return false; } } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java index 29d0fa57f78..6cf7be5d037 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2AnonymousWebFilter.java @@ -20,8 +20,10 @@ import java.net.URI; import org.apache.syncope.sra.security.PublicRouteMatcher; +import org.apache.syncope.sra.session.SessionUtils; import org.springframework.http.HttpStatus; -import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; @@ -29,24 +31,22 @@ public class SAML2AnonymousWebFilter implements WebFilter { - public static final String INITIAL_REQUEST_URI = "INITIAL_REQUEST_URI"; - - private final PublicRouteMatcher publicRouteMatcher; + private final ServerWebExchangeMatcher matcher; public SAML2AnonymousWebFilter(final PublicRouteMatcher publicRouteMatcher) { - this.publicRouteMatcher = publicRouteMatcher; + this.matcher = ServerWebExchangeMatchers.matchers( + publicRouteMatcher, + SessionUtils.authInSession()); } @Override public Mono filter(final ServerWebExchange exchange, final WebFilterChain chain) { - return publicRouteMatcher.matches(exchange). + return matcher.matches(exchange). filter(matchResult -> !matchResult.isMatch()). - flatMap(r -> exchange.getSession()).flatMap(r -> exchange.getSession()). - filter(s -> !s.getAttributes().containsKey( - WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME)). switchIfEmpty(chain.filter(exchange).then(Mono.empty())). + flatMap(r -> exchange.getSession()). flatMap(session -> { - session.getAttributes().put(INITIAL_REQUEST_URI, exchange.getRequest().getURI()); + session.getAttributes().put(SessionUtils.INITIAL_REQUEST_URI, exchange.getRequest().getURI()); exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER); exchange.getResponse().getHeaders(). diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java index fd2a924c0c0..b595d577847 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java @@ -18,7 +18,7 @@ */ package org.apache.syncope.sra.security.saml2; -import org.apache.syncope.sra.security.pac4j.ServerHttpContext; +import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import java.net.URI; import org.pac4j.core.exception.http.RedirectionAction; import org.pac4j.core.exception.http.WithContentAction; @@ -38,13 +38,13 @@ protected SAML2RequestGenerator(final SAML2Client saml2Client) { protected Mono handle( final RedirectionAction action, - final ServerHttpContext shc) { + final ServerWebExchangeContext swec) { if (action instanceof WithLocationAction) { WithLocationAction withLocationAction = (WithLocationAction) action; - shc.getNative().getResponse().setStatusCode(HttpStatus.FOUND); - shc.getNative().getResponse().getHeaders().setLocation(URI.create(withLocationAction.getLocation())); - return shc.getNative().getResponse().setComplete(); + swec.getNative().getResponse().setStatusCode(HttpStatus.FOUND); + swec.getNative().getResponse().getHeaders().setLocation(URI.create(withLocationAction.getLocation())); + return swec.getNative().getResponse().setComplete(); } else if (action instanceof WithContentAction) { WithContentAction withContentAction = (WithContentAction) action; String content = withContentAction.getContent(); @@ -54,9 +54,9 @@ protected Mono handle( } return Mono.defer(() -> { - shc.getNative().getResponse().getHeaders().setContentType(MediaType.TEXT_HTML); - return shc.getNative().getResponse(). - writeWith(Mono.just(shc.getNative().getResponse().bufferFactory().wrap(content.getBytes()))); + swec.getNative().getResponse().getHeaders().setContentType(MediaType.TEXT_HTML); + return swec.getNative().getResponse(). + writeWith(Mono.just(swec.getNative().getResponse().bufferFactory().wrap(content.getBytes()))); }); } else { throw new IllegalArgumentException("Unsupported Action: " + action.getClass().getName()); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java index 4b711090ea6..63001da83b6 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java @@ -24,6 +24,7 @@ import org.pac4j.saml.client.SAML2Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.web.server.SecurityWebFiltersOrder; @@ -40,8 +41,7 @@ public final class SAML2SecurityConfigUtils { private static final Logger LOG = LoggerFactory.getLogger(SAML2SecurityConfigUtils.class); private static ReactiveAuthenticationManager authenticationManager() { - return authentication -> Mono.just(authentication). - filter(Authentication::isAuthenticated); + return authentication -> Mono.just(authentication).filter(Authentication::isAuthenticated); } public static void forLogin( @@ -68,14 +68,14 @@ public static void forLogin( public static void forLogout( final ServerHttpSecurity.AuthorizeExchangeSpec builder, final SAML2Client saml2Client, + final CacheManager cacheManager, final LogoutRouteMatcher logoutRouteMatcher, final ConfigurableApplicationContext ctx) { LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); logoutWebFilter.setRequiresLogoutMatcher(logoutRouteMatcher); - SAML2ServerLogoutHandler logoutHandler = new SAML2ServerLogoutHandler(saml2Client); - logoutWebFilter.setLogoutHandler(logoutHandler); + logoutWebFilter.setLogoutHandler(new SAML2ServerLogoutHandler(saml2Client, cacheManager)); try { SAML2ServerLogoutSuccessHandler handler = ApplicationContextUtils.getOrCreateBean(ctx, @@ -83,8 +83,7 @@ public static void forLogout( SAML2ServerLogoutSuccessHandler.class); logoutWebFilter.setLogoutSuccessHandler(handler); } catch (ClassNotFoundException e) { - LOG.error("While creating instance of {}", - SAML2ServerLogoutSuccessHandler.class.getName(), e); + LOG.error("While creating instance of {}", SAML2ServerLogoutSuccessHandler.class.getName(), e); } builder.and().addFilterAt(logoutWebFilter, SecurityWebFiltersOrder.LOGOUT); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java index 0bcba311eb3..09590651f74 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java @@ -18,11 +18,13 @@ */ package org.apache.syncope.sra.security.saml2; -import org.apache.syncope.sra.security.pac4j.ServerHttpContext; +import org.apache.syncope.sra.SessionConfig; +import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.credentials.SAML2Credentials; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; import org.springframework.security.core.Authentication; import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; @@ -32,8 +34,11 @@ public class SAML2ServerLogoutHandler extends SAML2RequestGenerator implements S private static final Logger LOG = LoggerFactory.getLogger(SAML2ServerLogoutHandler.class); - public SAML2ServerLogoutHandler(final SAML2Client saml2Client) { + private final CacheManager cacheManager; + + public SAML2ServerLogoutHandler(final SAML2Client saml2Client, final CacheManager cacheManager) { super(saml2Client); + this.cacheManager = cacheManager; } @Override @@ -45,11 +50,12 @@ public Mono logout(final WebFilterExchange exchange, final Authentication LOG.debug("Creating SAML2 SP Logout Request for IDP[{}] and Profile[{}]", saml2Client.getIdentityProviderResolvedEntityId(), credentials.getUserProfile()); - ServerHttpContext shc = new ServerHttpContext(exchange.getExchange(), session); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange.getExchange(), session); + cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); return session.invalidate().then( - saml2Client.getLogoutAction(shc, credentials.getUserProfile(), null). - map(action -> handle(action, shc)). + saml2Client.getLogoutAction(swec, credentials.getUserProfile(), null). + map(action -> handle(action, swec)). orElseThrow(() -> new IllegalStateException("No action generated"))); }).onErrorResume(Mono::error); } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java index e57faad18ee..ab1819c38d8 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationRequestWebFilter.java @@ -18,7 +18,7 @@ */ package org.apache.syncope.sra.security.saml2; -import org.apache.syncope.sra.security.pac4j.ServerHttpContext; +import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.saml.client.SAML2Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,10 +57,10 @@ public Mono filter(final ServerWebExchange exchange, final WebFilterChain LOG.debug("Creating SAML2 SP Authentication Request for IDP[{}]", saml2Client.getIdentityProviderResolvedEntityId()); - ServerHttpContext shc = new ServerHttpContext(exchange, session); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange, session); - return saml2Client.getRedirectionAction(shc). - map(action -> handle(action, shc)). + return saml2Client.getRedirectionAction(swec). + map(action -> handle(action, swec)). orElseThrow(() -> new IllegalStateException("No action generated")); }).onErrorResume(Mono::error); } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java index 79ef94b31e5..0cbd9f77628 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2WebSsoAuthenticationWebFilter.java @@ -18,9 +18,10 @@ */ package org.apache.syncope.sra.security.saml2; -import org.apache.syncope.sra.security.pac4j.ServerHttpContext; import java.net.URI; +import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.apache.syncope.sra.security.web.server.DoNothingIfCommittedServerRedirectStrategy; +import org.apache.syncope.sra.session.SessionUtils; import org.pac4j.core.util.Pac4jConstants; import org.pac4j.saml.client.SAML2Client; import org.pac4j.saml.credentials.SAML2Credentials; @@ -87,12 +88,12 @@ private ServerAuthenticationConverter convertSamlResponse() { flatMap(form -> this.matcher.matches(exchange). flatMap(matchResult -> exchange.getSession()). flatMap(session -> { - ServerHttpContext shc = new ServerHttpContext(exchange, session).setForm(form); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange, session).setForm(form); - SAML2Credentials credentials = saml2Client.getCredentialsExtractor().extract(shc). + SAML2Credentials credentials = saml2Client.getCredentialsExtractor().extract(swec). orElseThrow(() -> new IllegalStateException("No AuthnResponse found")); - saml2Client.getAuthenticator().validate(credentials, shc); + saml2Client.getAuthenticator().validate(credentials, swec); return Mono.just(new SAML2AuthenticationToken(credentials)); })); @@ -110,7 +111,7 @@ public Mono onAuthenticationSuccess( return webFilterExchange.getExchange().getSession(). flatMap(session -> this.redirectStrategy.sendRedirect( webFilterExchange.getExchange(), - (URI) session.getRequiredAttribute(SAML2AnonymousWebFilter.INITIAL_REQUEST_URI))); + session.getRequiredAttribute(SessionUtils.INITIAL_REQUEST_URI))); } }; } diff --git a/sra/src/main/java/org/apache/syncope/sra/session/SessionUtils.java b/sra/src/main/java/org/apache/syncope/sra/session/SessionUtils.java new file mode 100644 index 00000000000..0f0b490250e --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/session/SessionUtils.java @@ -0,0 +1,39 @@ +/* + * 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. + */ +package org.apache.syncope.sra.session; + +import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; + +public final class SessionUtils { + + public static final String INITIAL_REQUEST_URI = "INITIAL_REQUEST_URI"; + + public static ServerWebExchangeMatcher authInSession() { + return exchange -> exchange.getSession(). + filter(session -> session.getAttributes().containsKey( + WebSessionServerSecurityContextRepository.DEFAULT_SPRING_SECURITY_CONTEXT_ATTR_NAME)). + flatMap(session -> ServerWebExchangeMatcher.MatchResult.match()). + switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch()); + } + + private SessionUtils() { + // private constructor for static utility class + } +} diff --git a/sra/src/test/resources/debug/application-debug.properties b/sra/src/test/resources/debug/application-debug.properties index 5af528f5a32..b74d3a4e336 100644 --- a/sra/src/test/resources/debug/application-debug.properties +++ b/sra/src/test/resources/debug/application-debug.properties @@ -30,16 +30,20 @@ #am.oauth2.client.id=oauth2TestClientId #am.oauth2.client.secret=oauth2TestClientSecret -am.type=SAML2 -am.saml2.sp.authnrequest.binding=POST -am.saml2.sp.logout.request.binding=POST -am.saml2.sp.logout.response.binding=POST -am.saml2.sp.entityId=http://localhost:8080 -am.saml2.sp.skew=300 -am.saml2.idp=http://localhost:9080/syncope-wa/idp/metadata -am.saml2.keystore=classpath:/saml.keystore.jks -am.saml2.keystore.type=jks -am.saml2.keystore.storepass=changeit -am.saml2.keystore.keypass=changeit +#am.type=SAML2 +#am.saml2.sp.authnrequest.binding=POST +#am.saml2.sp.logout.request.binding=POST +#am.saml2.sp.logout.response.binding=POST +#am.saml2.sp.entityId=http://localhost:8080 +#am.saml2.sp.skew=300 +#am.saml2.idp=http://localhost:9080/syncope-wa/idp/metadata +#am.saml2.keystore=classpath:/saml.keystore.jks +#am.saml2.keystore.type=jks +#am.saml2.keystore.storepass=changeit +#am.saml2.keystore.keypass=changeit + +am.type=CAS +am.cas.server.name=http://localhost:80 +am.cas.url.prefix=http://localhost:9080/syncope-wa/ global.postLogout=http://localhost:8080/logout From 321eb371410b4a612fe1070e6aac2d779da2cb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 4 Aug 2020 16:31:10 +0200 Subject: [PATCH 2/3] Removing println --- .../src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java | 1 - 1 file changed, 1 deletion(-) diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java index 7361e4f04a2..27b4d73e209 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java @@ -222,7 +222,6 @@ public void web() throws IOException { // 2d. post SAML response responseBody = EntityUtils.toString(response.getEntity()); - System.out.println("XXXXXXXXXXXXXXXXXXX3\n" + responseBody); parsed = parseSAMLResponseForm(responseBody); post = new HttpPost(parsed.getLeft()); From 298fd411eb930f3a90029a1461b7b5a4b5bd9c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Wed, 5 Aug 2020 07:00:53 +0200 Subject: [PATCH 3/3] Ensure to properly keep encoded query params --- .../syncope/fit/sra/AbstractITCase.java | 10 ++++- .../apache/syncope/fit/sra/CASSRAITCase.java | 7 +-- .../apache/syncope/fit/sra/OIDCSRAITCase.java | 4 +- .../syncope/fit/sra/SAML2SRAITCase.java | 4 +- .../syncope/sra/security/cas/CASUtils.java | 43 +++++++++---------- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java index db6e6a0bf4b..9ff4b976523 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/AbstractITCase.java @@ -44,6 +44,7 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import org.apache.commons.lang3.StringUtils; import org.apache.cxf.jaxrs.client.WebClient; import org.apache.http.Consts; import org.apache.http.HttpStatus; @@ -96,6 +97,9 @@ public abstract class AbstractITCase { protected static final String SRA_ADDRESS = "http://localhost:" + PORT; + protected static final String QUERY_STRING = + "key1=value1&key2=value2&key2=value3&key3=an%20url%20encoded%20value%3A%20this%21"; + protected static final String LOGGED_OUT_HEADER = "X-LOGGED-OUT"; protected static SyncopeClientFactoryBean clientFactory; @@ -332,12 +336,16 @@ protected static ObjectNode checkGetResponse( assertEquals("value2", key2.get(0).asText()); assertEquals("value3", key2.get(1).asText()); + assertEquals("an url encoded value: this!", args.get("key3").asText()); + ObjectNode headers = (ObjectNode) json.get("headers"); assertEquals(MediaType.TEXT_HTML, headers.get(HttpHeaders.ACCEPT).asText()); assertEquals(EN_LANGUAGE, headers.get(HttpHeaders.ACCEPT_LANGUAGE).asText()); assertEquals("localhost:" + PORT, headers.get("X-Forwarded-Host").asText()); - assertEquals(originalRequestURI, json.get("url").asText()); + assertEquals( + StringUtils.substringBefore(originalRequestURI, "?"), + StringUtils.substringBefore(json.get("url").asText(), "?")); return headers; } diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java index 611d5cce7e4..2b0e59a04a9 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/CASSRAITCase.java @@ -18,9 +18,6 @@ */ package org.apache.syncope.fit.sra; -import static org.apache.syncope.fit.sra.AbstractITCase.EN_LANGUAGE; -import static org.apache.syncope.fit.sra.AbstractITCase.WA_ADDRESS; -import static org.apache.syncope.fit.sra.AbstractITCase.extractCASExecution; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.fail; @@ -98,7 +95,7 @@ public void web() throws IOException { context.setCookieStore(new BasicCookieStore()); // 1. public - HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?key1=value1&key2=value2&key2=value3"); + HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?" + QUERY_STRING); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); CloseableHttpResponse response = httpclient.execute(get, context); @@ -107,7 +104,7 @@ public void web() throws IOException { assertFalse(headers.has(HttpHeaders.COOKIE)); // 2. protected - get = new HttpGet(SRA_ADDRESS + "/protected/get?key1=value1&key2=value2&key2=value3"); + get = new HttpGet(SRA_ADDRESS + "/protected/get?" + QUERY_STRING); String originalRequestURI = get.getURI().toASCIIString(); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java index 191cd32d113..d0b98f13325 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/OIDCSRAITCase.java @@ -144,7 +144,7 @@ public void web() throws IOException { context.setCookieStore(new BasicCookieStore()); // 1. public - HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?key1=value1&key2=value2&key2=value3"); + HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?" + QUERY_STRING); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); CloseableHttpResponse response = httpclient.execute(get, context); @@ -153,7 +153,7 @@ public void web() throws IOException { assertFalse(headers.has(HttpHeaders.COOKIE)); // 2. protected - get = new HttpGet(SRA_ADDRESS + "/protected/get?key1=value1&key2=value2&key2=value3"); + get = new HttpGet(SRA_ADDRESS + "/protected/get?" + QUERY_STRING); String originalRequestURI = get.getURI().toASCIIString(); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); diff --git a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java index 27b4d73e209..e4e11f21dcd 100644 --- a/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java +++ b/fit/wa-reference/src/test/java/org/apache/syncope/fit/sra/SAML2SRAITCase.java @@ -155,7 +155,7 @@ public void web() throws IOException { context.setCookieStore(new BasicCookieStore()); // 1. public - HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?key1=value1&key2=value2&key2=value3"); + HttpGet get = new HttpGet(SRA_ADDRESS + "/public/get?" + QUERY_STRING); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); CloseableHttpResponse response = httpclient.execute(get, context); @@ -164,7 +164,7 @@ public void web() throws IOException { assertFalse(headers.has(HttpHeaders.COOKIE)); // 2. protected - get = new HttpGet(SRA_ADDRESS + "/protected/get?key1=value1&key2=value2&key2=value3"); + get = new HttpGet(SRA_ADDRESS + "/protected/get?" + QUERY_STRING); String originalRequestURI = get.getURI().toASCIIString(); get.addHeader(HttpHeaders.ACCEPT, MediaType.TEXT_HTML); get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java index 5e2da27e107..1f1931f76f4 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/cas/CASUtils.java @@ -19,6 +19,7 @@ package org.apache.syncope.sra.security.cas; import java.util.List; +import org.apache.commons.lang3.StringUtils; import org.jasig.cas.client.Protocol; import org.jasig.cas.client.util.URIBuilder; import org.slf4j.Logger; @@ -63,15 +64,16 @@ public static String constructServiceUrl( UriComponents requestURI = UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).build(); - URIBuilder originalRequestUrl = new URIBuilder(requestURI.toUriString(), false); + URIBuilder originalRequestUrl = new URIBuilder( + StringUtils.substringBefore(requestURI.toUriString(), "?"), true); originalRequestUrl.setParameters(requestURI.getQuery()); URIBuilder builder; if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) { String scheme = exchange.getRequest().getSslInfo() == null ? "http://" : "https://"; - builder = new URIBuilder(scheme + serverName, false); + builder = new URIBuilder(scheme + serverName, true); } else { - builder = new URIBuilder(serverName, false); + builder = new URIBuilder(serverName, true); } builder.setPort(requestURI.getPort()); @@ -80,25 +82,22 @@ public static String constructServiceUrl( List serviceParameterNames = List.of(protocol.getServiceParameterName().split(",")); if (!serviceParameterNames.isEmpty() && !originalRequestUrl.getQueryParams().isEmpty()) { - originalRequestUrl.getQueryParams().forEach(pair -> { - String name = pair.getName(); - if (!name.equals(protocol.getArtifactParameterName()) && !serviceParameterNames.contains(name)) { - if (name.contains("&") || name.contains("=")) { - URIBuilder encodedParamBuilder = new URIBuilder(); - encodedParamBuilder.setParameters(name); - encodedParamBuilder.getQueryParams().forEach(pair2 -> { - String name2 = pair2.getName(); - if (!name2.equals(protocol.getArtifactParameterName()) - && !serviceParameterNames.contains(name2)) { - - builder.addParameter(name2, pair2.getValue()); - } - }); - } else { - builder.addParameter(name, pair.getValue()); - } - } - }); + originalRequestUrl.getQueryParams().stream(). + filter(pair -> !pair.getName().equals(protocol.getArtifactParameterName()) + && !serviceParameterNames.contains(pair.getName())). + forEach(pair -> { + String name = pair.getName(); + if (name.contains("&") || name.contains("=")) { + URIBuilder encodedParamBuilder = new URIBuilder(); + encodedParamBuilder.setParameters(name); + encodedParamBuilder.getQueryParams().stream(). + filter(pair2 -> !pair2.getName().equals(protocol.getArtifactParameterName()) + && !serviceParameterNames.contains(pair2.getName())). + forEach(pair2 -> builder.addParameter(pair2.getName(), pair2.getValue())); + } else { + builder.addParameter(name, pair.getValue()); + } + }); } String result = builder.toString();