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.core.logic;
20  
21  import com.nimbusds.jwt.JWTClaimsSet;
22  import com.nimbusds.jwt.SignedJWT;
23  import java.io.OutputStream;
24  import java.io.OutputStreamWriter;
25  import java.lang.reflect.Method;
26  import java.text.ParseException;
27  import java.time.OffsetDateTime;
28  import java.util.Base64;
29  import java.util.Date;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  import java.util.Optional;
34  import java.util.concurrent.ConcurrentHashMap;
35  import org.apache.commons.lang3.StringUtils;
36  import org.apache.commons.lang3.tuple.Pair;
37  import org.apache.syncope.common.lib.Attr;
38  import org.apache.syncope.common.lib.SyncopeClientException;
39  import org.apache.syncope.common.lib.saml2.SAML2LoginResponse;
40  import org.apache.syncope.common.lib.saml2.SAML2Request;
41  import org.apache.syncope.common.lib.saml2.SAML2Response;
42  import org.apache.syncope.common.lib.to.EntityTO;
43  import org.apache.syncope.common.lib.to.Item;
44  import org.apache.syncope.common.lib.to.UserTO;
45  import org.apache.syncope.common.lib.types.CipherAlgorithm;
46  import org.apache.syncope.common.lib.types.ClientExceptionType;
47  import org.apache.syncope.common.lib.types.IdRepoEntitlement;
48  import org.apache.syncope.common.lib.types.SAML2BindingType;
49  import org.apache.syncope.core.logic.saml2.NoOpSessionStore;
50  import org.apache.syncope.core.logic.saml2.SAML2ClientCache;
51  import org.apache.syncope.core.logic.saml2.SAML2SP4UIContext;
52  import org.apache.syncope.core.logic.saml2.SAML2SP4UIUserManager;
53  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
54  import org.apache.syncope.core.persistence.api.dao.SAML2SP4UIIdPDAO;
55  import org.apache.syncope.core.persistence.api.entity.Implementation;
56  import org.apache.syncope.core.persistence.api.entity.SAML2SP4UIIdP;
57  import org.apache.syncope.core.provisioning.api.RequestedAuthnContextProvider;
58  import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
59  import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
60  import org.apache.syncope.core.spring.implementation.ImplementationManager;
61  import org.apache.syncope.core.spring.security.AuthContextUtils;
62  import org.apache.syncope.core.spring.security.AuthDataAccessor;
63  import org.apache.syncope.core.spring.security.Encryptor;
64  import org.opensaml.saml.common.xml.SAMLConstants;
65  import org.opensaml.saml.saml2.core.AuthnRequest;
66  import org.opensaml.saml.saml2.core.LogoutResponse;
67  import org.opensaml.saml.saml2.core.NameID;
68  import org.opensaml.saml.saml2.core.RequestedAuthnContext;
69  import org.opensaml.saml.saml2.core.StatusCode;
70  import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
71  import org.opensaml.saml.saml2.metadata.EntityDescriptor;
72  import org.opensaml.saml.saml2.metadata.impl.AssertionConsumerServiceBuilder;
73  import org.pac4j.core.context.WebContext;
74  import org.pac4j.core.context.session.SessionStore;
75  import org.pac4j.core.exception.http.RedirectionAction;
76  import org.pac4j.core.exception.http.WithContentAction;
77  import org.pac4j.core.exception.http.WithLocationAction;
78  import org.pac4j.core.logout.NoLogoutActionBuilder;
79  import org.pac4j.saml.client.SAML2Client;
80  import org.pac4j.saml.config.SAML2Configuration;
81  import org.pac4j.saml.context.SAML2MessageContext;
82  import org.pac4j.saml.credentials.SAML2Credentials;
83  import org.pac4j.saml.credentials.authenticator.SAML2Authenticator;
84  import org.pac4j.saml.metadata.SAML2ServiceProviderMetadataResolver;
85  import org.pac4j.saml.profile.SAML2Profile;
86  import org.pac4j.saml.redirect.SAML2RedirectionActionBuilder;
87  import org.pac4j.saml.sso.impl.SAML2AuthnRequestBuilder;
88  import org.springframework.beans.BeanUtils;
89  import org.springframework.core.io.support.ResourcePatternResolver;
90  import org.springframework.security.access.prepost.PreAuthorize;
91  import org.springframework.util.ResourceUtils;
92  
93  public class SAML2SP4UILogic extends AbstractSAML2SP4UILogic {
94  
95      protected static final String JWT_CLAIM_IDP_ENTITYID = "IDP_ENTITYID";
96  
97      protected static final String JWT_CLAIM_NAMEID_FORMAT = "NAMEID_FORMAT";
98  
99      protected static final String JWT_CLAIM_NAMEID_VALUE = "NAMEID_VALUE";
100 
101     protected static final String JWT_CLAIM_SESSIONINDEX = "SESSIONINDEX";
102 
103     protected static final Encryptor ENCRYPTOR = Encryptor.getInstance();
104 
105     protected final AccessTokenDataBinder accessTokenDataBinder;
106 
107     protected final SAML2ClientCache saml2ClientCacheLogin;
108 
109     protected final SAML2ClientCache saml2ClientCacheLogout;
110 
111     protected final SAML2SP4UIUserManager userManager;
112 
113     protected final SAML2SP4UIIdPDAO idpDAO;
114 
115     protected final AuthDataAccessor authDataAccessor;
116 
117     protected final Map<String, String> metadataCache = new ConcurrentHashMap<>();
118 
119     protected final Map<String, RequestedAuthnContextProvider> perContextRACP = new ConcurrentHashMap<>();
120 
121     public SAML2SP4UILogic(
122             final SAML2SP4UIProperties props,
123             final ResourcePatternResolver resourceResolver,
124             final AccessTokenDataBinder accessTokenDataBinder,
125             final SAML2ClientCache saml2ClientCacheLogin,
126             final SAML2ClientCache saml2ClientCacheLogout,
127             final SAML2SP4UIUserManager userManager,
128             final SAML2SP4UIIdPDAO idpDAO,
129             final AuthDataAccessor authDataAccessor) {
130 
131         super(props, resourceResolver);
132 
133         this.accessTokenDataBinder = accessTokenDataBinder;
134         this.saml2ClientCacheLogin = saml2ClientCacheLogin;
135         this.saml2ClientCacheLogout = saml2ClientCacheLogout;
136         this.userManager = userManager;
137         this.idpDAO = idpDAO;
138         this.authDataAccessor = authDataAccessor;
139     }
140 
141     protected static String validateUrl(final String url) {
142         boolean isValid = true;
143         if (url.contains("..")) {
144             isValid = false;
145         }
146         if (isValid) {
147             isValid = ResourceUtils.isUrl(url);
148         }
149 
150         if (!isValid) {
151             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
152             sce.getElements().add("Invalid URL: " + url);
153             throw sce;
154         }
155 
156         return url;
157     }
158 
159     protected static String getCallbackUrl(final String spEntityID, final String urlContext) {
160         return validateUrl(spEntityID + urlContext + "/assertion-consumer");
161     }
162 
163     @PreAuthorize("isAuthenticated()")
164     public void getMetadata(final String spEntityID, final String urlContext, final OutputStream os) {
165         String metadata = metadataCache.get(spEntityID + urlContext);
166         if (metadata == null) {
167             SAML2Configuration cfg = newSAML2Configuration();
168             cfg.setServiceProviderEntityId(spEntityID);
169             cfg.setCallbackUrl(getCallbackUrl(spEntityID, urlContext));
170             SAML2ClientCache.getSPMetadataPath(spEntityID).ifPresent(cfg::setServiceProviderMetadataResourceFilepath);
171 
172             EntityDescriptor entityDescriptor =
173                     (EntityDescriptor) new SAML2ServiceProviderMetadataResolver(cfg).getEntityDescriptorElement();
174 
175             AssertionConsumerService postACS = entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).
176                     getAssertionConsumerServices().get(0);
177 
178             AssertionConsumerService redirectACS = new AssertionConsumerServiceBuilder().buildObject();
179             BeanUtils.copyProperties(postACS, redirectACS);
180             postACS.setBinding(SAML2BindingType.REDIRECT.getUri());
181             postACS.setIndex(1);
182             entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).
183                     getAssertionConsumerServices().add(redirectACS);
184 
185             entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices().
186                     removeIf(slo -> !SAML2BindingType.POST.getUri().equals(slo.getBinding())
187                     && !SAML2BindingType.REDIRECT.getUri().equals(slo.getBinding()));
188             entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS).getSingleLogoutServices().
189                     forEach(slo -> slo.setLocation(
190                     getCallbackUrl(spEntityID, urlContext).replace("/assertion-consumer", "/logout")));
191 
192             try {
193                 metadata = cfg.toMetadataGenerator().getMetadata(entityDescriptor);
194                 metadataCache.put(spEntityID + urlContext, metadata);
195             } catch (Exception e) {
196                 LOG.error("While generating SP metadata", e);
197                 SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
198                 sce.getElements().add(e.getMessage());
199                 throw sce;
200             }
201         }
202 
203         try (OutputStreamWriter osw = new OutputStreamWriter(os)) {
204             osw.write(metadata);
205         } catch (Exception e) {
206             LOG.error("While getting SP metadata", e);
207             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
208             sce.getElements().add(e.getMessage());
209             throw sce;
210         }
211     }
212 
213     protected SAML2Client getSAML2Client(
214             final SAML2ClientCache saml2ClientCache,
215             final SAML2SP4UIIdP idp,
216             final String spEntityID,
217             final String urlContext) {
218 
219         return saml2ClientCache.get(idp.getEntityID(), spEntityID).
220                 orElseGet(() -> saml2ClientCache.add(
221                 idp, newSAML2Configuration(), spEntityID, getCallbackUrl(spEntityID, urlContext)));
222     }
223 
224     protected SAML2Client getSAML2Client(
225             final SAML2ClientCache saml2ClientCache,
226             final String idpEntityID,
227             final String spEntityID,
228             final String urlContext) {
229 
230         SAML2SP4UIIdP idp = Optional.ofNullable(idpDAO.findByEntityID(idpEntityID)).
231                 orElseThrow(() -> new NotFoundException("SAML 2.0 IdP '" + idpEntityID + '\''));
232 
233         return getSAML2Client(saml2ClientCache, idp, spEntityID, urlContext);
234     }
235 
236     protected static SAML2Request buildRequest(final String idpEntityID, final RedirectionAction action) {
237         SAML2Request requestTO = new SAML2Request();
238         requestTO.setIdpEntityID(idpEntityID);
239         if (action instanceof WithLocationAction) {
240             WithLocationAction withLocationAction = (WithLocationAction) action;
241 
242             requestTO.setBindingType(SAML2BindingType.REDIRECT);
243             requestTO.setContent(withLocationAction.getLocation());
244         } else if (action instanceof WithContentAction) {
245             WithContentAction withContentAction = (WithContentAction) action;
246 
247             requestTO.setBindingType(SAML2BindingType.POST);
248             requestTO.setContent(Base64.getMimeEncoder().encodeToString(withContentAction.getContent().getBytes()));
249         }
250         return requestTO;
251     }
252 
253     protected Optional<RequestedAuthnContextProvider> getRequestedAuthnContextProvider(final SAML2SP4UIIdP idp) {
254         Implementation impl = idp.getRequestedAuthnContextProvider();
255         if (impl != null) {
256             try {
257                 return Optional.of(ImplementationManager.build(
258                         impl,
259                         () -> perContextRACP.get(impl.getKey()),
260                         instance -> perContextRACP.put(impl.getKey(), instance)));
261             } catch (Exception e) {
262                 LOG.warn("Cannot instantiate '{}', reverting to default behavior", impl, e);
263             }
264         }
265 
266         return Optional.empty();
267     }
268 
269     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
270     public SAML2Request createLoginRequest(
271             final String spEntityID,
272             final String urlContext,
273             final String idpEntityID) {
274 
275         // 0. look for IdP
276         SAML2SP4UIIdP idp = Optional.ofNullable(idpDAO.findByEntityID(idpEntityID)).
277                 orElseThrow(() -> new NotFoundException("SAML 2.0 IdP '" + idpEntityID + '\''));
278 
279         // 1. look for configured client
280         SAML2Client saml2Client = getSAML2Client(saml2ClientCacheLogin, idp, spEntityID, urlContext);
281 
282         getRequestedAuthnContextProvider(idp).ifPresent(requestedAuthnContextProvider -> {
283             RequestedAuthnContext requestedAuthnContext = requestedAuthnContextProvider.get();
284             saml2Client.setRedirectionActionBuilder(new SAML2RedirectionActionBuilder(saml2Client) {
285 
286                 @Override
287                 public Optional<RedirectionAction> getRedirectionAction(
288                         final WebContext wc, final SessionStore sessionStore) {
289 
290                     this.saml2ObjectBuilder = new SAML2AuthnRequestBuilder() {
291 
292                         @Override
293                         public AuthnRequest build(final SAML2MessageContext context) {
294                             AuthnRequest authnRequest = super.build(context);
295                             authnRequest.setRequestedAuthnContext(requestedAuthnContext);
296                             return authnRequest;
297                         }
298                     };
299                     return super.getRedirectionAction(wc, sessionStore);
300                 }
301             });
302         });
303 
304         // 2. create AuthnRequest
305         SAML2SP4UIContext ctx = new SAML2SP4UIContext(
306                 saml2Client.getConfiguration().getAuthnRequestBindingType(),
307                 null);
308         RedirectionAction action = saml2Client.getRedirectionAction(ctx, NoOpSessionStore.INSTANCE).
309                 orElseThrow(() -> {
310                     SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
311                     sce.getElements().add("No RedirectionAction generated for AuthnRequest");
312                     return sce;
313                 });
314         return buildRequest(idpEntityID, action);
315     }
316 
317     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
318     public SAML2LoginResponse validateLoginResponse(final SAML2Response saml2Response) {
319         // 0. look for IdP
320         SAML2SP4UIIdP idp = Optional.ofNullable(idpDAO.findByEntityID(saml2Response.getIdpEntityID())).
321                 orElseThrow(() -> new NotFoundException("SAML 2.0 IdP '" + saml2Response.getIdpEntityID() + '\''));
322 
323         // 1. look for configured client
324         SAML2Client saml2Client = getSAML2Client(
325                 saml2ClientCacheLogin,
326                 idp,
327                 saml2Response.getSpEntityID(),
328                 saml2Response.getUrlContext());
329 
330         // 2. validate the provided SAML response
331         SAML2Credentials credentials;
332         try {
333             SAML2SP4UIContext ctx = new SAML2SP4UIContext(
334                     saml2Client.getConfiguration().getAuthnRequestBindingType(),
335                     saml2Response);
336 
337             credentials = (SAML2Credentials) saml2Client.getCredentialsExtractor().
338                     extract(ctx, NoOpSessionStore.INSTANCE).
339                     orElseThrow(() -> new IllegalStateException("No AuthnResponse found"));
340 
341             saml2Client.getAuthenticator().validate(credentials, ctx, NoOpSessionStore.INSTANCE);
342         } catch (Exception e) {
343             LOG.error("While validating AuthnResponse", e);
344             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
345             sce.getElements().add(e.getMessage());
346             throw sce;
347         }
348 
349         // 3. prepare the result: find matching user (if any) and return the received attributes
350         SAML2LoginResponse loginResp = new SAML2LoginResponse();
351         loginResp.setIdp(saml2Client.getIdentityProviderResolvedEntityId());
352         loginResp.setSloSupported(!(saml2Client.getLogoutActionBuilder() instanceof NoLogoutActionBuilder));
353 
354         SAML2Credentials.SAMLNameID nameID = credentials.getNameId();
355 
356         Item connObjectKeyItem = idp.getConnObjectKeyItem().orElse(null);
357 
358         String keyValue = null;
359         if (StringUtils.isNotBlank(nameID.getValue())
360                 && connObjectKeyItem != null
361                 && connObjectKeyItem.getExtAttrName().equals(NameID.DEFAULT_ELEMENT_LOCAL_NAME)) {
362 
363             keyValue = nameID.getValue();
364         }
365 
366         loginResp.setNotOnOrAfter(new Date(credentials.getConditions().getNotOnOrAfter().toInstant().toEpochMilli()));
367 
368         loginResp.setSessionIndex(credentials.getSessionIndex());
369 
370         for (SAML2Credentials.SAMLAttribute attr : credentials.getAttributes()) {
371             if (!attr.getAttributeValues().isEmpty()) {
372                 String attrName = Optional.ofNullable(attr.getFriendlyName()).orElse(attr.getName());
373                 if (connObjectKeyItem != null && attrName.equals(connObjectKeyItem.getExtAttrName())) {
374                     keyValue = attr.getAttributeValues().get(0);
375                 }
376 
377                 loginResp.getAttrs().add(new Attr.Builder(attrName).values(attr.getAttributeValues()).build());
378             }
379         }
380 
381         List<String> matchingUsers = Optional.ofNullable(keyValue).
382                 map(k -> userManager.findMatchingUser(k, idp.getKey())).
383                 orElse(List.of());
384         LOG.debug("Found {} matching users for {}", matchingUsers.size(), keyValue);
385 
386         String username;
387         if (matchingUsers.isEmpty()) {
388             if (idp.isCreateUnmatching()) {
389                 LOG.debug("No user matching {}, about to create", keyValue);
390 
391                 username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
392                         () -> userManager.create(idp, loginResp, nameID.getValue()));
393             } else if (idp.isSelfRegUnmatching()) {
394                 loginResp.setNameID(nameID.getValue());
395                 UserTO userTO = new UserTO();
396 
397                 userManager.fill(idp.getKey(), loginResp, userTO);
398 
399                 loginResp.getAttrs().clear();
400                 loginResp.getAttrs().addAll(userTO.getPlainAttrs());
401                 if (StringUtils.isNotBlank(userTO.getUsername())) {
402                     loginResp.setUsername(userTO.getUsername());
403                 } else {
404                     loginResp.setUsername(keyValue);
405                 }
406 
407                 loginResp.setSelfReg(true);
408 
409                 return loginResp;
410             } else {
411                 throw new NotFoundException("User matching the provided value " + keyValue);
412             }
413         } else if (matchingUsers.size() > 1) {
414             throw new IllegalArgumentException("Several users match the provided value " + keyValue);
415         } else {
416             if (idp.isUpdateMatching()) {
417                 LOG.debug("About to update {} for {}", matchingUsers.get(0), keyValue);
418 
419                 username = AuthContextUtils.callAsAdmin(AuthContextUtils.getDomain(),
420                         () -> userManager.update(matchingUsers.get(0), idp, loginResp));
421             } else {
422                 username = matchingUsers.get(0);
423             }
424         }
425 
426         loginResp.setUsername(username);
427         loginResp.setNameID(nameID.getValue());
428 
429         // 4. generate JWT for further access
430         Map<String, Object> claims = new HashMap<>();
431         claims.put(JWT_CLAIM_IDP_ENTITYID, idp.getEntityID());
432         claims.put(JWT_CLAIM_NAMEID_FORMAT, nameID.getFormat());
433         claims.put(JWT_CLAIM_NAMEID_VALUE, nameID.getValue());
434         claims.put(JWT_CLAIM_SESSIONINDEX, loginResp.getSessionIndex());
435 
436         byte[] authorities = null;
437         try {
438             authorities = ENCRYPTOR.encode(POJOHelper.serialize(
439                     authDataAccessor.getAuthorities(loginResp.getUsername(), null)), CipherAlgorithm.AES).getBytes();
440         } catch (Exception e) {
441             LOG.error("Could not fetch authorities", e);
442         }
443 
444         Pair<String, OffsetDateTime> accessTokenInfo =
445                 accessTokenDataBinder.create(loginResp.getUsername(), claims, authorities, true);
446         loginResp.setAccessToken(accessTokenInfo.getLeft());
447         loginResp.setAccessTokenExpiryTime(accessTokenInfo.getRight());
448 
449         return loginResp;
450     }
451 
452     @PreAuthorize("isAuthenticated() and not(hasRole('" + IdRepoEntitlement.ANONYMOUS + "'))")
453     public SAML2Request createLogoutRequest(
454             final String accessToken,
455             final String spEntityID,
456             final String urlContext) {
457 
458         // 1. fetch the current JWT used for Syncope authentication
459         JWTClaimsSet claimsSet;
460         try {
461             SignedJWT jwt = SignedJWT.parse(accessToken);
462             claimsSet = jwt.getJWTClaimsSet();
463         } catch (ParseException e) {
464             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidAccessToken);
465             sce.getElements().add(e.getMessage());
466             throw sce;
467         }
468 
469         // 2. look for SAML2Client
470         String idpEntityID = (String) claimsSet.getClaim(JWT_CLAIM_IDP_ENTITYID);
471         if (idpEntityID == null) {
472             throw new NotFoundException("No SAML 2.0 IdP information found in the access token");
473         }
474         SAML2Client saml2Client = getSAML2Client(saml2ClientCacheLogout, idpEntityID, spEntityID, urlContext);
475         if (saml2Client.getLogoutActionBuilder() instanceof NoLogoutActionBuilder) {
476             throw new IllegalArgumentException("No SingleLogoutService available for "
477                     + saml2Client.getIdentityProviderResolvedEntityId());
478         }
479 
480         // 3. create LogoutRequest
481         SAML2Profile saml2Profile = new SAML2Profile();
482         saml2Profile.setId((String) claimsSet.getClaim(JWT_CLAIM_NAMEID_VALUE));
483         saml2Profile.addAuthenticationAttribute(
484                 SAML2Authenticator.SAML_NAME_ID_FORMAT,
485                 claimsSet.getClaim(JWT_CLAIM_NAMEID_FORMAT));
486         saml2Profile.addAuthenticationAttribute(
487                 SAML2Authenticator.SESSION_INDEX,
488                 claimsSet.getClaim(JWT_CLAIM_SESSIONINDEX));
489 
490         SAML2SP4UIContext ctx = new SAML2SP4UIContext(
491                 saml2Client.getConfiguration().getSpLogoutRequestBindingType(), null);
492         RedirectionAction action = saml2Client.getLogoutAction(
493                 ctx,
494                 NoOpSessionStore.INSTANCE,
495                 saml2Profile, null).
496                 orElseThrow(() -> {
497                     SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Unknown);
498                     sce.getElements().add("No RedirectionAction generated for LogoutRequest");
499                     return sce;
500                 });
501         return buildRequest(idpEntityID, action);
502     }
503 
504     @PreAuthorize("hasRole('" + IdRepoEntitlement.ANONYMOUS + "')")
505     public void validateLogoutResponse(final SAML2Response saml2Response) {
506         // 1. look for SAML2Client
507         if (saml2Response.getIdpEntityID() == null) {
508             LOG.error("No SAML 2.0 IdP entityID provided, ignoring");
509             return;
510         }
511         SAML2Client saml2Client = getSAML2Client(
512                 saml2ClientCacheLogout,
513                 saml2Response.getIdpEntityID(),
514                 saml2Response.getSpEntityID(),
515                 saml2Response.getUrlContext());
516 
517         Optional.ofNullable(idpDAO.findByEntityID(saml2Client.getIdentityProviderResolvedEntityId())).
518                 orElseThrow(() -> new NotFoundException(
519                 "SAML 2.0 IdP '" + saml2Client.getIdentityProviderResolvedEntityId() + '\''));
520 
521         // 2. validate the provided SAML response
522         SAML2SP4UIContext ctx = new SAML2SP4UIContext(
523                 saml2Client.getConfiguration().getSpLogoutRequestBindingType(),
524                 saml2Response);
525 
526         LogoutResponse logoutResponse;
527         try {
528             SAML2MessageContext saml2Ctx = saml2Client.getContextProvider().
529                     buildContext(saml2Client, ctx, NoOpSessionStore.INSTANCE);
530             saml2Client.getLogoutProfileHandler().receive(saml2Ctx);
531 
532             logoutResponse = (LogoutResponse) saml2Ctx.getMessageContext().getMessage();
533         } catch (Exception e) {
534             LOG.error("Could not validate LogoutResponse", e);
535             return;
536         }
537 
538         // 3. finally check for logout status
539         if (!StatusCode.SUCCESS.equals(logoutResponse.getStatus().getStatusCode().getValue())) {
540             LOG.warn("Logout from SAML 2.0 IdP '{}' was not successful",
541                     saml2Client.getIdentityProviderResolvedEntityId());
542         }
543     }
544 
545     @Override
546     protected EntityTO resolveReference(
547             final Method method, final Object... args) throws UnresolvedReferenceException {
548 
549         throw new UnresolvedReferenceException();
550     }
551 }