package org.apereo.cas.support.saml.services.idp.metadata;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import org.apereo.cas.support.saml.SamlIdPUtils;
import org.apereo.cas.support.saml.services.SamlRegisteredService;
import org.apereo.cas.support.saml.services.idp.metadata.cache.SamlRegisteredServiceCachingMetadataResolver;
import org.apereo.cas.util.DateTimeUtils;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.opensaml.saml.criterion.BindingCriterion;
import org.opensaml.saml.metadata.resolver.ChainingMetadataResolver;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.opensaml.saml.saml2.core.RequestAbstractType;
import org.opensaml.saml.saml2.metadata.AssertionConsumerService;
import org.opensaml.saml.saml2.metadata.ContactPerson;
import org.opensaml.saml.saml2.metadata.EntityDescriptor;
import org.opensaml.saml.saml2.metadata.Extensions;
import org.opensaml.saml.saml2.metadata.KeyDescriptor;
import org.opensaml.saml.saml2.metadata.NameIDFormat;
import org.opensaml.saml.saml2.metadata.Organization;
import org.opensaml.saml.saml2.metadata.SPSSODescriptor;
import org.opensaml.saml.saml2.metadata.SingleLogoutService;
import org.opensaml.xmlsec.signature.Signature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* This is {@link SamlRegisteredServiceServiceProviderMetadataFacade} that acts a façade between the SAML metadata resolved
* from a metadata resource and other outer layers in CAS that need access to the bits of that
* metadata. Once the metadata resolved for a service is adapted and parsed for a given entity id,
* callers will be able to peek into the various configuration elements of the metadata to handle
* further saml processing. A metadata adaptor is always linked to a saml service.
*
* @author Misagh Moayyed
* @since 5.0.0
*/
public final class SamlRegisteredServiceServiceProviderMetadataFacade {
private static final Logger LOGGER = LoggerFactory.getLogger(SamlRegisteredServiceServiceProviderMetadataFacade.class);
private final SPSSODescriptor ssoDescriptor;
private final EntityDescriptor entityDescriptor;
private final MetadataResolver metadataResolver;
private SamlRegisteredServiceServiceProviderMetadataFacade(final SPSSODescriptor ssoDescriptor,
final EntityDescriptor entityDescriptor,
final MetadataResolver metadataResolver) {
this.ssoDescriptor = ssoDescriptor;
this.entityDescriptor = entityDescriptor;
this.metadataResolver = metadataResolver;
}
/**
* Adapt saml metadata and parse. Acts as a facade.
*
* @param resolver the resolver
* @param registeredService the service
* @param entityID the entity id
* @return the saml metadata adaptor
*/
public static Optional<SamlRegisteredServiceServiceProviderMetadataFacade> get(final SamlRegisteredServiceCachingMetadataResolver resolver,
final SamlRegisteredService registeredService,
final String entityID) {
return get(resolver, registeredService, entityID, new CriteriaSet());
}
/**
* Adapt saml metadata and parse. Acts as a facade.
*
* @param resolver the resolver
* @param registeredService the service
* @param request the request
* @return the saml metadata adaptor
*/
public static Optional<SamlRegisteredServiceServiceProviderMetadataFacade> get(final SamlRegisteredServiceCachingMetadataResolver resolver,
final SamlRegisteredService registeredService,
final RequestAbstractType request) {
return get(resolver, registeredService, SamlIdPUtils.getIssuerFromSamlRequest(request));
}
private static Optional<SamlRegisteredServiceServiceProviderMetadataFacade> get(final SamlRegisteredServiceCachingMetadataResolver resolver,
final SamlRegisteredService registeredService,
final String entityID,
final CriteriaSet criterions) {
LOGGER.info("Adapting SAML metadata for CAS service [{}] issued by [{}]",
registeredService.getName(), entityID);
try {
criterions.add(new BindingCriterion(Collections.singletonList(SAMLConstants.SAML2_POST_BINDING_URI)));
criterions.add(new EntityIdCriterion(entityID));
LOGGER.info("Locating metadata for entityID [{}] with binding [{}] by attempting to run through the metadata chain...",
entityID, SAMLConstants.SAML2_POST_BINDING_URI);
final ChainingMetadataResolver chainingMetadataResolver = resolver.resolve(registeredService);
LOGGER.info("Resolved metadata chain for service [{}]. Filtering the chain by entity ID [{}] and binding [{}]",
registeredService.getServiceId(), entityID, SAMLConstants.SAML2_POST_BINDING_URI);
final EntityDescriptor entityDescriptor = chainingMetadataResolver.resolveSingle(criterions);
if (entityDescriptor == null) {
LOGGER.debug("Cannot find entity [{}] in metadata provider.", entityID);
return Optional.empty();
}
LOGGER.debug("Located EntityDescriptor in metadata for [{}]", entityID);
final SPSSODescriptor ssoDescriptor = entityDescriptor.getSPSSODescriptor(SAMLConstants.SAML20P_NS);
if (ssoDescriptor != null) {
LOGGER.debug("Located SPSSODescriptor in metadata for [{}]. Metadata is valid until [{}]",
entityID, ssoDescriptor.getValidUntil());
return Optional.of(new SamlRegisteredServiceServiceProviderMetadataFacade(ssoDescriptor, entityDescriptor, chainingMetadataResolver));
}
LOGGER.warn("Could not locate SPSSODescriptor in the metadata for [{}]", entityID);
return Optional.empty();
} catch (final Exception e) {
throw new RuntimeException(e.getMessage(), e);
}
}
public SPSSODescriptor getSsoDescriptor() {
return this.ssoDescriptor;
}
public ZonedDateTime getValidUntil() {
return DateTimeUtils.zonedDateTimeOf(this.ssoDescriptor.getValidUntil());
}
public EntityDescriptor getEntityDescriptor() {
return this.entityDescriptor;
}
public Organization getOrganization() {
return this.ssoDescriptor.getOrganization();
}
public Signature getSignature() {
return this.ssoDescriptor.getSignature();
}
public List<ContactPerson> getContactPersons() {
return this.ssoDescriptor.getContactPersons();
}
public long getCacheDuration() {
return this.ssoDescriptor.getCacheDuration();
}
public List<KeyDescriptor> getKeyDescriptors() {
return this.ssoDescriptor.getKeyDescriptors();
}
public Extensions getExtensions() {
return this.ssoDescriptor.getExtensions();
}
public List<String> getSupportedProtocols() {
return this.ssoDescriptor.getSupportedProtocols();
}
public boolean isWantAssertionsSigned() {
return this.ssoDescriptor.getWantAssertionsSigned();
}
public boolean isAuthnRequestsSigned() {
return this.ssoDescriptor.isAuthnRequestsSigned();
}
/**
* Is supported protocol?
*
* @param protocol the protocol
* @return true/false
*/
public boolean isSupportedProtocol(final String protocol) {
return this.ssoDescriptor.isSupportedProtocol(protocol);
}
/**
* Gets entity id.
*
* @return the entity id
*/
public String getEntityId() {
return this.entityDescriptor.getEntityID();
}
/**
* Gets supported name formats.
*
* @return the supported name formats
*/
public List<String> getSupportedNameIdFormats() {
final List<String> nameIdFormats = new ArrayList<>();
final List<XMLObject> children = this.ssoDescriptor.getOrderedChildren();
if (children != null) {
nameIdFormats.addAll(children.stream().filter(NameIDFormat.class::isInstance)
.map(child -> ((NameIDFormat) child).getFormat()).collect(Collectors.toList()));
}
return nameIdFormats;
}
private List<AssertionConsumerService> getAssertionConsumerServices() {
return (List) this.ssoDescriptor.getEndpoints(AssertionConsumerService.DEFAULT_ELEMENT_NAME);
}
public List<SingleLogoutService> getSingleLogoutServices() {
return (List) this.ssoDescriptor.getEndpoints(SingleLogoutService.DEFAULT_ELEMENT_NAME);
}
public SingleLogoutService getSingleLogoutService() {
return getSingleLogoutServices().get(0);
}
/**
* Gets assertion consumer service.
*
* @param binding the binding
* @return the assertion consumer service
*/
public AssertionConsumerService getAssertionConsumerService(final String binding) {
return getAssertionConsumerServices().stream().filter(acs -> acs.getBinding().equals(binding)).findFirst().orElse(null);
}
private AssertionConsumerService getAssertionConsumerServiceForPaosBinding() {
return getAssertionConsumerService(SAMLConstants.SAML2_PAOS_BINDING_URI);
}
private AssertionConsumerService getAssertionConsumerServiceForPostBinding() {
return getAssertionConsumerService(SAMLConstants.SAML2_POST_BINDING_URI);
}
public MetadataResolver getMetadataResolver() {
return this.metadataResolver;
}
/**
* Contains assertion consumer services ?
*
* @return true/false
*/
public boolean containsAssertionConsumerServices() {
return !getAssertionConsumerServices().isEmpty();
}
}