View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.sra.security.saml2;
20  
21  import java.util.Optional;
22  import org.apache.syncope.sra.SessionConfig;
23  import org.apache.syncope.sra.security.pac4j.NoOpSessionStore;
24  import org.apache.syncope.sra.security.pac4j.RedirectionActionUtils;
25  import org.apache.syncope.sra.security.pac4j.ServerWebExchangeContext;
26  import org.pac4j.core.exception.http.OkAction;
27  import org.pac4j.core.exception.http.RedirectionAction;
28  import org.pac4j.core.util.Pac4jConstants;
29  import org.pac4j.saml.client.SAML2Client;
30  import org.pac4j.saml.context.SAML2MessageContext;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  import org.springframework.cache.CacheManager;
34  import org.springframework.http.HttpMethod;
35  import org.springframework.security.web.server.WebFilterExchange;
36  import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
37  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher;
38  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher.MatchResult;
39  import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
40  import org.springframework.web.server.ServerWebExchange;
41  import org.springframework.web.server.WebFilter;
42  import org.springframework.web.server.WebFilterChain;
43  import reactor.core.publisher.Mono;
44  
45  public class SAML2LogoutResponseWebFilter implements WebFilter {
46  
47      private static final Logger LOG = LoggerFactory.getLogger(SAML2LogoutResponseWebFilter.class);
48  
49      public static final ServerWebExchangeMatcher MATCHER =
50              ServerWebExchangeMatchers.pathMatchers("/logout/saml2/sso");
51  
52      private static class ServerWebExchangeLogoutContext extends ServerWebExchangeContext {
53  
54          ServerWebExchangeLogoutContext(final ServerWebExchange exchange) {
55              super(exchange);
56          }
57  
58          @Override
59          public Optional<String> getRequestParameter(final String name) {
60              return Pac4jConstants.LOGOUT_ENDPOINT_PARAMETER.equals(name)
61                      ? Optional.of("true")
62                      : super.getRequestParameter(name);
63          }
64      }
65  
66      private final SAML2Client saml2Client;
67  
68      private final ServerLogoutSuccessHandler logoutSuccessHandler;
69  
70      private final CacheManager cacheManager;
71  
72      public SAML2LogoutResponseWebFilter(
73              final SAML2Client saml2Client,
74              final SAML2ServerLogoutSuccessHandler logoutSuccessHandler,
75              final CacheManager cacheManager) {
76  
77          this.saml2Client = saml2Client;
78          this.logoutSuccessHandler = logoutSuccessHandler;
79          this.cacheManager = cacheManager;
80      }
81  
82      private Mono<Void> handleLogoutResponse(
83              final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) {
84  
85          try {
86              SAML2MessageContext ctx = saml2Client.getContextProvider().
87                  buildContext(this.saml2Client, swec, NoOpSessionStore.INSTANCE);
88              saml2Client.getLogoutProfileHandler().receive(ctx);
89          } catch (OkAction e) {
90              LOG.debug("LogoutResponse was actually validated but no postLogoutURL was set", e);
91          } catch (Exception e) {
92              LOG.error("Could not validate LogoutResponse", e);
93          }
94  
95          return logoutSuccessHandler.onLogoutSuccess(new WebFilterExchange(exchange, chain), null);
96      }
97  
98      private Mono<Void> handleLogoutRequest(
99              final ServerWebExchange exchange, final WebFilterChain chain, final ServerWebExchangeContext swec) {
100 
101         return exchange.getSession().
102                 switchIfEmpty(chain.filter(exchange).then(Mono.empty())).
103                 flatMap(session -> {
104                     cacheManager.getCache(SessionConfig.DEFAULT_CACHE).evictIfPresent(session.getId());
105 
106                     return session.invalidate().then(Mono.defer(() -> {
107                         try {
108                             saml2Client.getCredentialsExtractor().extract(swec, NoOpSessionStore.INSTANCE);
109                         } catch (RedirectionAction action) {
110                             return RedirectionActionUtils.handle(action, swec);
111                         }
112 
113                         return chain.filter(exchange).then(Mono.empty());
114                     }));
115                 });
116     }
117 
118     private Mono<Void> handleGET(final ServerWebExchange exchange, final WebFilterChain chain) {
119         if (exchange.getRequest().getQueryParams().getFirst("SAMLResponse") != null) {
120             return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange));
121         } else if (exchange.getRequest().getQueryParams().getFirst("SAMLRequest") != null) {
122             return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange));
123         }
124 
125         return chain.filter(exchange).then(Mono.empty());
126     }
127 
128     private Mono<Void> handlePOST(final ServerWebExchange exchange, final WebFilterChain chain) {
129         return exchange.getFormData().flatMap(form -> {
130             if (form.containsKey("SAMLResponse")) {
131                 return handleLogoutResponse(exchange, chain, new ServerWebExchangeContext(exchange).setForm(form));
132             } else if (form.containsKey("SAMLRequest")) {
133                 return handleLogoutRequest(exchange, chain, new ServerWebExchangeLogoutContext(exchange).setForm(form));
134             }
135 
136             return chain.filter(exchange).then(Mono.empty());
137         });
138     }
139 
140     @Override
141     public Mono<Void> filter(final ServerWebExchange exchange, final WebFilterChain chain) {
142         return MATCHER.matches(exchange).
143                 filter(MatchResult::isMatch).
144                 switchIfEmpty(chain.filter(exchange).then(Mono.empty())).
145                 flatMap(matchResult -> {
146                     return exchange.getRequest().getMethod() == HttpMethod.GET
147                             ? handleGET(exchange, chain)
148                             : exchange.getRequest().getMethod() == HttpMethod.POST
149                             ? handlePOST(exchange, chain)
150                             : Mono.error(() -> new UnsupportedOperationException(
151                             "Unsupported HTTP method: " + exchange.getRequest().getMethod()));
152                 });
153     }
154 }