package org.pac4j.saml.metadata;
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
import net.shibboleth.utilities.java.support.resolver.ResolverException;
import org.opensaml.core.criterion.EntityIdCriterion;
import org.opensaml.core.xml.XMLObject;
import org.opensaml.core.xml.io.MarshallingException;
import org.opensaml.saml.metadata.resolver.MetadataResolver;
import org.pac4j.core.context.HttpConstants;
import org.pac4j.core.exception.TechnicalException;
import org.pac4j.core.util.CommonHelper;
import org.pac4j.saml.client.SAML2ClientConfiguration;
import org.pac4j.saml.crypto.CredentialProvider;
import org.pac4j.saml.exceptions.SAMLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.WritableResource;
import javax.annotation.Nullable;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.URL;
/**
* @author Misagh Moayyed
* @since 1.7
*/
public class SAML2ServiceProviderMetadataResolver implements SAML2MetadataResolver {
protected final static Logger logger = LoggerFactory.getLogger(SAML2ServiceProviderMetadataResolver.class);
private final CredentialProvider credentialProvider;
private String spEntityId;
private final WritableResource spMetadataResource;
private String spMetadata;
private final String callbackUrl;
private final boolean forceSpMetadataGeneration;
private boolean authnRequestSigned;
private boolean wantsAssertionsSigned;
public SAML2ServiceProviderMetadataResolver(final SAML2ClientConfiguration configuration,
final String callbackUrl,
final CredentialProvider credentialProvider) {
this(configuration.getServiceProviderMetadataResource(), callbackUrl,
configuration.getServiceProviderEntityId(), configuration.isForceServiceProviderMetadataGeneration(), credentialProvider,
configuration.isAuthnRequestSigned(), configuration.getWantsAssertionsSigned());
}
private SAML2ServiceProviderMetadataResolver(final WritableResource spMetadataResource,
final String callbackUrl,
@Nullable final String spEntityId,
final boolean forceSpMetadataGeneration,
final CredentialProvider credentialProvider,
boolean authnRequestSigned, boolean wantsAssertionsSigned) {
this.authnRequestSigned = authnRequestSigned;
this.wantsAssertionsSigned = wantsAssertionsSigned;
this.spMetadataResource = spMetadataResource;
this.spEntityId = spEntityId;
this.credentialProvider = credentialProvider;
this.callbackUrl = callbackUrl;
this.forceSpMetadataGeneration = forceSpMetadataGeneration;
// If the spEntityId is blank, use the callback url
try {
if (CommonHelper.isBlank(this.spEntityId)) {
final URL url = new URL(callbackUrl);
if (url.getQuery() != null) {
this.spEntityId = url.toString().replace("?" + url.getQuery(), "");
} else {
this.spEntityId = url.toString();
}
}
logger.info("Using SP entity ID {}", this.spEntityId);
} catch (final Exception e) {
throw new SAMLException(e);
}
}
@Override
public final MetadataResolver resolve() {
if (this.authnRequestSigned && this.credentialProvider == null) {
throw new TechnicalException("Credentials Provider can not be null when authnRequestSigned is set to true");
}
try {
final SAML2MetadataGenerator metadataGenerator = new SAML2MetadataGenerator();
metadataGenerator.setWantAssertionSigned(this.wantsAssertionsSigned);
metadataGenerator.setAuthnRequestSigned(this.authnRequestSigned);
if (this.authnRequestSigned) {
metadataGenerator.setCredentialProvider(this.credentialProvider);
}
metadataGenerator.setEntityId(this.spEntityId);
metadataGenerator.setRequestInitiatorLocation(callbackUrl);
// Assertion consumer service url is the callback url
metadataGenerator.setAssertionConsumerServiceUrl(callbackUrl);
// for now same for logout url
metadataGenerator.setSingleLogoutServiceUrl(callbackUrl);
final MetadataResolver spMetadataProvider = metadataGenerator.buildMetadataResolver();
// Initialize metadata provider for our SP and get the XML as a String
this.spMetadata = metadataGenerator.getMetadata();
if (this.spMetadataResource != null) {
if (spMetadataResource.exists() && !this.forceSpMetadataGeneration) {
logger.info("Metadata file already exists at {}.", this.spMetadataResource.getFilename());
} else {
logger.info("Writing sp metadata to {}", this.spMetadataResource.getFilename());
final File parent = spMetadataResource.getFile().getParentFile();
if (parent != null) {
logger.info("Attempting to create directory structure for: {}", parent.getCanonicalPath());
if (!parent.exists() && !parent.mkdirs()) {
logger.warn("Could not construct the directory structure for SP metadata: {}",
parent.getCanonicalPath());
}
}
final Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
final StreamResult result = new StreamResult(new StringWriter());
final StreamSource source = new StreamSource(new StringReader(this.spMetadata));
transformer.transform(source, result);
try (final OutputStream spMetadataOutputStream = this.spMetadataResource.getOutputStream()) {
spMetadataOutputStream.write(result.getWriter().toString().getBytes(HttpConstants.UTF8_ENCODING));
}
}
}
return spMetadataProvider;
} catch (final ComponentInitializationException e) {
throw new TechnicalException("Error initializing spMetadataProvider", e);
} catch (final MarshallingException e) {
logger.warn("Unable to marshal SP metadata", e);
} catch (final IOException e) {
logger.warn("Unable to print SP metadata", e);
} catch (final Exception e) {
logger.warn("Unable to transform metadata", e);
}
return null;
}
@Override
public final String getEntityId() {
return this.spEntityId;
}
@Override
public String getMetadataPath() {
if (this.spMetadataResource != null) {
return this.spMetadataResource.getFilename();
}
return null;
}
@Override
public String getMetadata() {
return this.spMetadata;
}
@Override
public XMLObject getEntityDescriptorElement() {
try {
return resolve().resolveSingle(new CriteriaSet(new EntityIdCriterion(getEntityId())));
} catch (final ResolverException e) {
throw new SAMLException("Error initializing idpMetadataProvider", e);
}
}
}