This is an automated email from the ASF dual-hosted git repository. ilgrosso pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/syncope.git
commit 8ce9aa6141336ba22044270e60030875dd598b23 Author: Francesco Chicchiriccò <[email protected]> AuthorDate: Tue Aug 11 14:55:35 2020 +0200 [SYNCOPE-1579] Finalizing SAML 2.0 logout --- .travis.yml | 2 +- .../org/apache/syncope/fit/sra/SAML2SRAITCase.java | 13 +- .../test/resources/application-saml2.properties | 2 +- .../org/apache/syncope/sra/SecurityConfig.java | 4 +- .../NoOpLogoutHandler.java} | 14 +- ...{WebSessionStore.java => NoOpSessionStore.java} | 23 ++-- .../RedirectionActionUtils.java} | 18 +-- .../security/pac4j/ServerWebExchangeContext.java | 29 +--- .../security/saml2/SAML2AnonymousWebFilter.java | 3 +- .../saml2/SAML2LogoutResponseWebFilter.java | 152 +++++++++++++++++++++ .../sra/security/saml2/SAML2MetadataEndpoint.java | 23 +++- ...r.java => SAML2RequestServerLogoutHandler.java} | 15 +- .../security/saml2/SAML2SecurityConfigUtils.java | 12 +- .../saml2/SAML2ServerLogoutSuccessHandler.java | 3 +- .../SAML2WebSsoAuthenticationRequestWebFilter.java | 28 ++-- .../saml2/SAML2WebSsoAuthenticationWebFilter.java | 25 +--- .../resources/debug/application-debug.properties | 28 ++-- 17 files changed, 257 insertions(+), 137 deletions(-) diff --git a/.travis.yml b/.travis.yml index f44f586..2beae45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -120,6 +120,6 @@ jobs: name: "Integration Tests: SRA and WA" script: mvn -f fit/wa-reference/pom.xml verify -Dinvoker.streamLogs=true -Dmodernizer.skip=true -Dianal.skip=true -Drat.skip=true -Dcheckstyle.skip=true -Djacoco.skip=true after_failure: + - cat fit/wa-reference/target/log/* - cat fit/core-reference/target/log/* - - cat fit/core-reference/target/failsafe-reports/org.apache.syncope.fit.*-output.txt ##################################################### 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 e4e11f2..2f1ff7c 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 @@ -22,7 +22,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -265,17 +264,7 @@ public class SAML2SRAITCase extends AbstractITCase { get.addHeader(HttpHeaders.ACCEPT_LANGUAGE, EN_LANGUAGE); response = httpclient.execute(get, context); - // 3b. post SAML response - // this is missing as currently WA does not responde with form for SP's SingleLogoutService + // 3b. check logout checkLogout(response); } - - @Override - protected void checkLogout(final CloseableHttpResponse response) throws IOException { - assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); - - String responseBody = EntityUtils.toString(response.getEntity()); - assertTrue(responseBody.contains("Logout successful")); - assertTrue(responseBody.contains("have successfully logged out of the Central Authentication Service")); - } } diff --git a/fit/wa-reference/src/test/resources/application-saml2.properties b/fit/wa-reference/src/test/resources/application-saml2.properties index fa9a8e4..70fd4d6 100644 --- a/fit/wa-reference/src/test/resources/application-saml2.properties +++ b/fit/wa-reference/src/test/resources/application-saml2.properties @@ -17,7 +17,7 @@ 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.logout.response.binding=REDIRECT am.saml2.sp.entityId=http://localhost:8080 am.saml2.sp.skew=300 am.saml2.idp=http://localhost:9080/syncope-wa/idp/metadata 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 9d9fda1..eeacaef 100644 --- a/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java +++ b/sra/src/main/java/org/apache/syncope/sra/SecurityConfig.java @@ -28,6 +28,7 @@ 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.pac4j.NoOpLogoutHandler; import org.apache.syncope.sra.security.saml2.SAML2BindingType; import org.apache.syncope.sra.security.saml2.SAML2MetadataEndpoint; import org.apache.syncope.sra.security.saml2.SAML2SecurityConfigUtils; @@ -229,11 +230,12 @@ public class SecurityConfig { cfg.setAuthnRequestSigned(true); cfg.setSpLogoutRequestSigned(true); cfg.setAcceptedSkew(env.getProperty("am.saml2.sp.skew", int.class)); + cfg.setLogoutHandler(new NoOpLogoutHandler()); SAML2Client saml2Client = new SAML2Client(cfg); saml2Client.setName(AMType.SAML2.name()); saml2Client.setCallbackUrl(env.getProperty("am.saml2.sp.entityId") - + SAML2WebSsoAuthenticationWebFilter.DEFAULT_FILTER_PROCESSES_URI); + + SAML2WebSsoAuthenticationWebFilter.FILTER_PROCESSES_URI); saml2Client.setCallbackUrlResolver(new NoParameterCallbackUrlResolver()); saml2Client.init(); diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpLogoutHandler.java similarity index 55% copy from sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java copy to sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpLogoutHandler.java index 3557eb5..e370826 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpLogoutHandler.java @@ -16,18 +16,10 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.syncope.sra.security.saml2; +package org.apache.syncope.sra.security.pac4j; -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; +import org.pac4j.core.logout.handler.LogoutHandler; -public class SAML2ServerLogoutSuccessHandler extends AbstractServerLogoutSuccessHandler { +public class NoOpLogoutHandler implements LogoutHandler<ServerWebExchangeContext> { - @Override - public Mono<Void> 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/pac4j/WebSessionStore.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpSessionStore.java similarity index 72% rename from sra/src/main/java/org/apache/syncope/sra/security/pac4j/WebSessionStore.java rename to sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpSessionStore.java index c74336c..e9629f4 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/WebSessionStore.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/NoOpSessionStore.java @@ -20,43 +20,44 @@ package org.apache.syncope.sra.security.pac4j; import java.util.Optional; import org.pac4j.core.context.session.SessionStore; -import org.springframework.web.server.WebSession; -public class WebSessionStore implements SessionStore<ServerWebExchangeContext> { +public final class NoOpSessionStore implements SessionStore<ServerWebExchangeContext> { - private final WebSession webSession; + public static final NoOpSessionStore INSTANCE = new NoOpSessionStore(); - public WebSessionStore(final WebSession webSession) { - this.webSession = webSession; + private NoOpSessionStore() { + // private constructor for singleton } @Override public String getOrCreateSessionId(final ServerWebExchangeContext context) { - return this.webSession.getId(); + return "<NO_KEY>"; } @Override public Optional<Object> get(final ServerWebExchangeContext context, final String key) { - return Optional.ofNullable(this.webSession.getAttribute(key)); + return Optional.empty(); } @Override public void set(final ServerWebExchangeContext context, final String key, final Object value) { + // nothing to do } @Override public boolean destroySession(final ServerWebExchangeContext context) { - return false; + return true; } @Override - public Optional<WebSession> getTrackableSession(final ServerWebExchangeContext context) { - return Optional.ofNullable(this.webSession); + public Optional<?> getTrackableSession(final ServerWebExchangeContext context) { + return Optional.empty(); } @Override public Optional<SessionStore<ServerWebExchangeContext>> buildFromTrackableSession( - final ServerWebExchangeContext context, final Object trackableSession) { + final ServerWebExchangeContext context, + final Object trackableSession) { return Optional.empty(); } 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/pac4j/RedirectionActionUtils.java similarity index 86% rename from sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java rename to sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java index b595d57..c56c23f 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestGenerator.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/RedirectionActionUtils.java @@ -16,27 +16,19 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.syncope.sra.security.saml2; +package org.apache.syncope.sra.security.pac4j; -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; import org.pac4j.core.exception.http.WithLocationAction; -import org.pac4j.saml.client.SAML2Client; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import reactor.core.publisher.Mono; -abstract class SAML2RequestGenerator { +public final class RedirectionActionUtils { - protected final SAML2Client saml2Client; - - protected SAML2RequestGenerator(final SAML2Client saml2Client) { - this.saml2Client = saml2Client; - } - - protected Mono<Void> handle( + public static Mono<Void> handle( final RedirectionAction action, final ServerWebExchangeContext swec) { @@ -62,4 +54,8 @@ abstract class SAML2RequestGenerator { throw new IllegalArgumentException("Unsupported Action: " + action.getClass().getName()); } } + + private RedirectionActionUtils() { + // private constructor for static utility class + } } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java index bf22e69..a5ed233 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/pac4j/ServerWebExchangeContext.java @@ -33,52 +33,29 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; -import org.springframework.web.server.WebSession; import org.springframework.web.util.UriComponentsBuilder; public class ServerWebExchangeContext implements WebContext { private final ServerWebExchange exchange; - private WebSessionStore sessionStore; - private MultiValueMap<String, String> form; private String body; /** - * Build a WebFlux context from the current exchange and web session. - * - * @param exchange the current exchange - * @param webSession the current web session - */ - public ServerWebExchangeContext(final ServerWebExchange exchange, final WebSession webSession) { - this(exchange, new WebSessionStore(webSession)); - } - - /** - * Build a WebFlux context from the current exhange and from a session store. + * Build a WebFlux context from the current exchange. * * @param exchange the current exchange - * @param sessionStore the session store to use */ - public ServerWebExchangeContext( - final ServerWebExchange exchange, - final WebSessionStore sessionStore) { - + public ServerWebExchangeContext(final ServerWebExchange exchange) { CommonHelper.assertNotNull("exchange", exchange); - CommonHelper.assertNotNull("sessionStore", sessionStore); this.exchange = exchange; - this.sessionStore = sessionStore; - } - - public WebSessionStore getNativeSessionStore() { - return this.sessionStore; } @Override public SessionStore<ServerWebExchangeContext> getSessionStore() { - return this.sessionStore; + return NoOpSessionStore.INSTANCE; } @Override 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 6cf7be5..b5f10c4 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 @@ -36,7 +36,8 @@ public class SAML2AnonymousWebFilter implements WebFilter { public SAML2AnonymousWebFilter(final PublicRouteMatcher publicRouteMatcher) { this.matcher = ServerWebExchangeMatchers.matchers( publicRouteMatcher, - SessionUtils.authInSession()); + SessionUtils.authInSession(), + SAML2LogoutResponseWebFilter.MATCHER); } @Override diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java new file mode 100644 index 0000000..79733ef --- /dev/null +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2LogoutResponseWebFilter.java @@ -0,0 +1,152 @@ +/* + * 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.saml2; + +import java.util.Optional; +import org.apache.syncope.sra.SessionConfig; +import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; +import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; +import org.pac4j.core.exception.http.OkAction; +import org.pac4j.core.exception.http.RedirectionAction; +import org.pac4j.core.util.Pac4jConstants; +import org.pac4j.saml.client.SAML2Client; +import org.pac4j.saml.context.SAML2MessageContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cache.CacheManager; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.server.WebFilterExchange; +import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; +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 SAML2LogoutResponseWebFilter implements WebFilter { + + private static final Logger LOG = LoggerFactory.getLogger(SAML2LogoutResponseWebFilter.class); + + public static final ServerWebExchangeMatcher MATCHER = + ServerWebExchangeMatchers.pathMatchers("/logout/saml2/sso"); + + private static class ServerWebExchangeLogoutContext extends ServerWebExchangeContext { + + ServerWebExchangeLogoutContext(final ServerWebExchange exchange) { + super(exchange); + } + + @Override + public Optional<String> getRequestParameter(final String name) { + return Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER.equals(name) + ? Optional.of("true") + : super.getRequestParameter(name); + } + } + + private final SAML2Client saml2Client; + + private final ServerLogoutSuccessHandler logoutSuccessHandler; + + private final CacheManager cacheManager; + + public SAML2LogoutResponseWebFilter( + final SAML2Client saml2Client, + final SAML2ServerLogoutSuccessHandler logoutSuccessHandler, + final CacheManager cacheManager) { + + this.saml2Client = saml2Client; + this.logoutSuccessHandler = logoutSuccessHandler; + this.cacheManager = cacheManager; + } + + private Mono<Void> handleLogoutResponse( + final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) { + + try { + SAML2MessageContext ctx = saml2Client.getContextProvider().buildContext(swec); + saml2Client.getLogoutProfileHandler().receive(ctx); + } catch (OkAction e) { + LOG.debug("LogoutResponse was actually validated but no postLogoutURL was set", e); + } catch (Exception e) { + LOG.error("Could not validate LogoutResponse", e); + } + + return logoutSuccessHandler.onLogoutSuccess(new WebFilterExchange(exchange, chain), null); + } + + private Mono<Void> handleLogoutRequest( + final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) { + + return exchange.getSession(). + switchIfEmpty(chain.filter(exchange).then(Mono.empty())). + flatMap(session -> { + cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); + + return session.invalidate().then(Mono.defer(() -> { + try { + saml2Client.getCredentialsExtractor().extract(swec); + } catch (RedirectionAction action) { + return RedirectionActionUtils.handle(action, swec); + } + + return chain.filter(exchange).then(Mono.empty()); + })); + }); + } + + private Mono<Void> handleGET(final ServerWebExchange exchange, final WebFilterChain chain) { + if (exchange.getRequest().getQueryParams().getFirst("SAMLResponse") != null) { + return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange)); + } else if (exchange.getRequest().getQueryParams().getFirst("SAMLRequest") != null) { + return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange)); + } + + return chain.filter(exchange).then(Mono.empty()); + } + + private Mono<Void> handlePOST(final ServerWebExchange exchange, final WebFilterChain chain) { + return exchange.getFormData().flatMap(form -> { + if (form.containsKey("SAMLResponse")) { + return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange).setForm(form)); + } else if (form.containsKey("SAMLRequest")) { + return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange).setForm(form)); + } + + return chain.filter(exchange).then(Mono.empty()); + }); + } + + @Override + public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) { + return MATCHER.matches(exchange). + filter(MatchResult::isMatch). + switchIfEmpty(chain.filter(exchange).then(Mono.empty())). + flatMap(matchResult -> { + return exchange.getRequest().getMethod() == HttpMethod.GET + ? handleGET(exchange, chain) + : exchange.getRequest().getMethod() == HttpMethod.POST + ? handlePOST(exchange, chain) + : Mono.error(() -> new UnsupportedOperationException( + "Unsupported HTTP method: " + exchange.getRequest().getMethod())); + }); + } +} diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java index 229d232..6c8efa6 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2MetadataEndpoint.java @@ -19,8 +19,12 @@ package org.apache.syncope.sra.security.saml2; import com.google.common.net.HttpHeaders; +import org.apache.commons.lang3.StringUtils; import org.apache.syncope.sra.SecurityConfig; +import org.opensaml.saml.common.xml.SAMLConstants; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; import org.pac4j.saml.client.SAML2Client; +import org.pac4j.saml.exceptions.SAMLException; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -38,10 +42,23 @@ public class SAML2MetadataEndpoint { public static final String METADATA_URL = "/saml2/metadata"; - private final SAML2Client saml2Client; + private final String metadata; public SAML2MetadataEndpoint(final SAML2Client saml2Client) { - this.saml2Client = saml2Client; + EntityDescriptor entityDescriptor = (EntityDescriptor) saml2Client.getServiceProviderMetadataResolver(). + getEntityDescriptorElement(); + entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices(). + removeIf(slo -> !saml2Client.getConfiguration(). + getSpLogoutResponseBindingType().equals(slo.getBinding())); + entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices(). + forEach(slo -> slo.setLocation( + StringUtils.substringBefore(slo.getLocation(), "?").replace("login", "logout"))); + + try { + this.metadata = saml2Client.getConfiguration().toMetadataGenerator().getMetadata(entityDescriptor); + } catch (Exception e) { + throw new SAMLException("Unable to fetch metadata", e); + } } @GetMapping(produces = { MediaType.APPLICATION_XML_VALUE }) @@ -49,6 +66,6 @@ public class SAML2MetadataEndpoint { public Mono<ResponseEntity<String>> metadata(final ServerHttpRequest request) { return Mono.just(ResponseEntity.ok(). header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_XML_VALUE). - body(saml2Client.getServiceProviderMetadataResolver().getMetadata())); + body(metadata)); } } 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/SAML2RequestServerLogoutHandler.java similarity index 83% rename from sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutHandler.java rename to sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2RequestServerLogoutHandler.java index 0959065..973bd20 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/SAML2RequestServerLogoutHandler.java @@ -18,6 +18,7 @@ */ package org.apache.syncope.sra.security.saml2; +import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; import org.apache.syncope.sra.SessionConfig; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.saml.client.SAML2Client; @@ -30,14 +31,16 @@ import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.ServerLogoutHandler; import reactor.core.publisher.Mono; -public class SAML2ServerLogoutHandler extends SAML2RequestGenerator implements ServerLogoutHandler { +public class SAML2RequestServerLogoutHandler implements ServerLogoutHandler { - private static final Logger LOG = LoggerFactory.getLogger(SAML2ServerLogoutHandler.class); + private static final Logger LOG = LoggerFactory.getLogger(SAML2RequestServerLogoutHandler.class); + + private final SAML2Client saml2Client; private final CacheManager cacheManager; - public SAML2ServerLogoutHandler(final SAML2Client saml2Client, final CacheManager cacheManager) { - super(saml2Client); + public SAML2RequestServerLogoutHandler(final SAML2Client saml2Client, final CacheManager cacheManager) { + this.saml2Client = saml2Client; this.cacheManager = cacheManager; } @@ -50,12 +53,12 @@ public class SAML2ServerLogoutHandler extends SAML2RequestGenerator implements S LOG.debug("Creating SAML2 SP Logout Request for IDP[{}] and Profile[{}]", saml2Client.getIdentityProviderResolvedEntityId(), credentials.getUserProfile()); - ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange.getExchange(), session); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange.getExchange()); cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId()); return session.invalidate().then( saml2Client.getLogoutAction(swec, credentials.getUserProfile(), null). - map(action -> handle(action, swec)). + map(action -> RedirectionActionUtils.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/SAML2SecurityConfigUtils.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2SecurityConfigUtils.java index 63001da..a7be69c 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 @@ -74,14 +74,18 @@ public final class SAML2SecurityConfigUtils { LogoutWebFilter logoutWebFilter = new LogoutWebFilter(); logoutWebFilter.setRequiresLogoutMatcher(logoutRouteMatcher); - - logoutWebFilter.setLogoutHandler(new SAML2ServerLogoutHandler(saml2Client, cacheManager)); + logoutWebFilter.setLogoutHandler(new SAML2RequestServerLogoutHandler(saml2Client, cacheManager)); + logoutWebFilter.setLogoutSuccessHandler((exchange, authentication) -> Mono.empty()); try { - SAML2ServerLogoutSuccessHandler handler = ApplicationContextUtils.getOrCreateBean(ctx, + SAML2ServerLogoutSuccessHandler logoutSuccessHandler = ApplicationContextUtils.getOrCreateBean( + ctx, SAML2ServerLogoutSuccessHandler.class.getName(), SAML2ServerLogoutSuccessHandler.class); - logoutWebFilter.setLogoutSuccessHandler(handler); + + SAML2LogoutResponseWebFilter logoutResponseWebFilter = + new SAML2LogoutResponseWebFilter(saml2Client, logoutSuccessHandler, cacheManager); + builder.and().addFilterAt(logoutResponseWebFilter, SecurityWebFiltersOrder.LOGOUT); } catch (ClassNotFoundException e) { LOG.error("While creating instance of {}", SAML2ServerLogoutSuccessHandler.class.getName(), e); } diff --git a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java index 3557eb5..f10e388 100644 --- a/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java +++ b/sra/src/main/java/org/apache/syncope/sra/security/saml2/SAML2ServerLogoutSuccessHandler.java @@ -27,7 +27,6 @@ public class SAML2ServerLogoutSuccessHandler extends AbstractServerLogoutSuccess @Override public Mono<Void> onLogoutSuccess(final WebFilterExchange exchange, final Authentication authentication) { - return Mono.just(authentication). - flatMap(auth -> redirectStrategy.sendRedirect(exchange.getExchange(), getPostLogout(exchange))); + return redirectStrategy.sendRedirect(exchange.getExchange(), getPostLogout(exchange)); } } 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 ab1819c..14a5b8d 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,49 +18,47 @@ */ package org.apache.syncope.sra.security.saml2; +import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils; import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext; import org.pac4j.saml.client.SAML2Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; +import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; -import org.springframework.util.Assert; 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 SAML2WebSsoAuthenticationRequestWebFilter extends SAML2RequestGenerator implements WebFilter { +public class SAML2WebSsoAuthenticationRequestWebFilter implements WebFilter { private static final Logger LOG = LoggerFactory.getLogger(SAML2WebSsoAuthenticationRequestWebFilter.class); public static final String AUTHENTICATE_URL = "/saml2/authenticate"; - private ServerWebExchangeMatcher redirectMatcher = ServerWebExchangeMatchers.pathMatchers(AUTHENTICATE_URL); + private static final ServerWebExchangeMatcher MATCHER = + ServerWebExchangeMatchers.pathMatchers(AUTHENTICATE_URL); - public SAML2WebSsoAuthenticationRequestWebFilter(final SAML2Client saml2Client) { - super(saml2Client); - } + private final SAML2Client saml2Client; - public void setRedirectMatcher(final ServerWebExchangeMatcher redirectMatcher) { - Assert.notNull(redirectMatcher, "redirectMatcher cannot be null"); - this.redirectMatcher = redirectMatcher; + public SAML2WebSsoAuthenticationRequestWebFilter(final SAML2Client saml2Client) { + this.saml2Client = saml2Client; } @Override public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) { - return redirectMatcher.matches(exchange). - filter(matchResult -> matchResult.isMatch()). + return MATCHER.matches(exchange). + filter(MatchResult::isMatch). switchIfEmpty(chain.filter(exchange).then(Mono.empty())). - flatMap(matchResult -> exchange.getSession()). - flatMap(session -> { + flatMap(matchResult -> { LOG.debug("Creating SAML2 SP Authentication Request for IDP[{}]", saml2Client.getIdentityProviderResolvedEntityId()); - ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange, session); + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange); return saml2Client.getRedirectionAction(swec). - map(action -> handle(action, swec)). + map(action -> RedirectionActionUtils.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 0cbd9f7..5dc59b3 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 @@ -22,7 +22,6 @@ 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; import org.springframework.security.authentication.ReactiveAuthenticationManager; @@ -32,7 +31,6 @@ 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.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; import org.springframework.web.server.ServerWebExchange; @@ -41,16 +39,12 @@ import reactor.core.publisher.Mono; public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter { - public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/saml2/sso"; + public static final String FILTER_PROCESSES_URI = "/login/saml2/sso"; - private final SAML2Client saml2Client; + private static final ServerWebExchangeMatcher MATCHER = + ServerWebExchangeMatchers.pathMatchers(FILTER_PROCESSES_URI); - private ServerWebExchangeMatcher matcher = new AndServerWebExchangeMatcher( - ServerWebExchangeMatchers.pathMatchers(DEFAULT_FILTER_PROCESSES_URI), - exchange -> exchange.getRequest().getQueryParams(). - containsKey(Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER) - ? ServerWebExchangeMatcher.MatchResult.notMatch() - : ServerWebExchangeMatcher.MatchResult.match()); + private final SAML2Client saml2Client; public SAML2WebSsoAuthenticationWebFilter( final ReactiveAuthenticationManager authenticationManager, @@ -67,10 +61,6 @@ public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter setAuthenticationSuccessHandler(redirectToInitialRequestURI()); } - public void setMatcher(final ServerWebExchangeMatcher matcher) { - this.matcher = matcher; - } - @Override public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) { return super.filter(exchange, chain).then(Mono.defer(exchange.getResponse()::setComplete)); @@ -85,10 +75,9 @@ public class SAML2WebSsoAuthenticationWebFilter extends AuthenticationWebFilter private ServerAuthenticationConverter convertSamlResponse() { return exchange -> exchange.getFormData(). - flatMap(form -> this.matcher.matches(exchange). - flatMap(matchResult -> exchange.getSession()). - flatMap(session -> { - ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange, session).setForm(form); + flatMap(form -> this.MATCHER.matches(exchange). + flatMap(matchResult -> { + ServerWebExchangeContext swec = new ServerWebExchangeContext(exchange).setForm(form); SAML2Credentials credentials = saml2Client.getCredentialsExtractor().extract(swec). orElseThrow(() -> new IllegalStateException("No AuthnResponse found")); diff --git a/sra/src/test/resources/debug/application-debug.properties b/sra/src/test/resources/debug/application-debug.properties index b74d3a4..c029337 100644 --- a/sra/src/test/resources/debug/application-debug.properties +++ b/sra/src/test/resources/debug/application-debug.properties @@ -30,20 +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=REDIRECT +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/ +#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
