322 lines
15 KiB
Java
322 lines
15 KiB
Java
package eu.eudat.logic.security.validators.configurableProvider;
|
|
|
|
import eu.eudat.logic.security.customproviders.ConfigurableProvider.entities.saml2.Saml2ConfigurableProvider;
|
|
import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
|
|
import net.shibboleth.utilities.java.support.resolver.CriteriaSet;
|
|
import net.shibboleth.utilities.java.support.xml.BasicParserPool;
|
|
import org.apache.http.impl.client.HttpClientBuilder;
|
|
import org.opensaml.core.config.ConfigurationService;
|
|
import org.opensaml.core.config.InitializationException;
|
|
import org.opensaml.core.config.InitializationService;
|
|
import org.opensaml.core.criterion.EntityIdCriterion;
|
|
import org.opensaml.core.xml.XMLObject;
|
|
import org.opensaml.core.xml.config.XMLObjectProviderRegistry;
|
|
import org.opensaml.core.xml.io.Unmarshaller;
|
|
import org.opensaml.core.xml.io.UnmarshallerFactory;
|
|
import org.opensaml.core.xml.io.UnmarshallingException;
|
|
import org.opensaml.core.xml.schema.*;
|
|
import org.opensaml.saml.common.xml.SAMLConstants;
|
|
import org.opensaml.saml.criterion.EntityRoleCriterion;
|
|
import org.opensaml.saml.criterion.ProtocolCriterion;
|
|
import org.opensaml.saml.metadata.resolver.impl.HTTPMetadataResolver;
|
|
import org.opensaml.saml.metadata.resolver.impl.PredicateRoleDescriptorResolver;
|
|
import org.opensaml.saml.saml2.core.*;
|
|
import org.opensaml.saml.saml2.metadata.IDPSSODescriptor;
|
|
import org.opensaml.saml.security.impl.MetadataCredentialResolver;
|
|
import org.opensaml.security.credential.Credential;
|
|
import org.opensaml.security.credential.UsageType;
|
|
import org.opensaml.security.criteria.UsageCriterion;
|
|
import org.opensaml.security.x509.X509Credential;
|
|
import org.opensaml.xml.util.Base64;
|
|
import org.opensaml.xmlsec.config.impl.DefaultSecurityConfigurationBootstrap;
|
|
import org.opensaml.xmlsec.keyinfo.KeyInfoCredentialResolver;
|
|
import org.opensaml.xmlsec.signature.support.SignatureValidator;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.w3c.dom.Document;
|
|
import org.w3c.dom.Element;
|
|
import org.xml.sax.SAXException;
|
|
|
|
import javax.xml.parsers.DocumentBuilder;
|
|
import javax.xml.parsers.DocumentBuilderFactory;
|
|
import javax.xml.parsers.ParserConfigurationException;
|
|
import java.io.*;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.*;
|
|
import java.util.stream.Collectors;
|
|
import java.util.zip.Inflater;
|
|
import java.util.zip.InflaterInputStream;
|
|
|
|
public class Saml2SSOUtils {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(Saml2SSOUtils.class);
|
|
private static boolean isBootStrapped = false;
|
|
private static BasicParserPool parserPool;
|
|
private static XMLObjectProviderRegistry registry;
|
|
|
|
private Saml2SSOUtils() {
|
|
}
|
|
|
|
private static void doBootstrap() throws Exception {
|
|
if (!isBootStrapped) {
|
|
try {
|
|
boostrap();
|
|
isBootStrapped = true;
|
|
} catch (Exception e) {
|
|
throw new Exception("Error in bootstrapping the OpenSAML2 library", e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void boostrap(){
|
|
parserPool = new BasicParserPool();
|
|
parserPool.setMaxPoolSize(100);
|
|
parserPool.setCoalescing(true);
|
|
parserPool.setIgnoreComments(true);
|
|
parserPool.setIgnoreElementContentWhitespace(true);
|
|
parserPool.setNamespaceAware(true);
|
|
parserPool.setExpandEntityReferences(false);
|
|
parserPool.setXincludeAware(false);
|
|
|
|
final Map<String, Boolean> features = new HashMap<String, Boolean>();
|
|
features.put("http://xml.org/sax/features/external-general-entities", Boolean.FALSE);
|
|
features.put("http://xml.org/sax/features/external-parameter-entities", Boolean.FALSE);
|
|
features.put("http://apache.org/xml/features/disallow-doctype-decl", Boolean.TRUE);
|
|
features.put("http://apache.org/xml/features/validation/schema/normalized-value", Boolean.FALSE);
|
|
features.put("http://javax.xml.XMLConstants/feature/secure-processing", Boolean.TRUE);
|
|
|
|
parserPool.setBuilderFeatures(features);
|
|
|
|
parserPool.setBuilderAttributes(new HashMap<String, Object>());
|
|
|
|
try {
|
|
parserPool.initialize();
|
|
} catch (ComponentInitializationException e) {
|
|
logger.error(e.getMessage(), e);
|
|
}
|
|
|
|
registry = new XMLObjectProviderRegistry();
|
|
ConfigurationService.register(XMLObjectProviderRegistry.class, registry);
|
|
registry.setParserPool(parserPool);
|
|
|
|
try {
|
|
InitializationService.initialize();
|
|
} catch (InitializationException e) {
|
|
logger.error(e.getMessage(), e);
|
|
}
|
|
}
|
|
|
|
public static String getAttributeName(Attribute attribute, Saml2ConfigurableProvider.SAML2UsingFormat usingFormat){
|
|
String friendlyName = attribute.getFriendlyName();
|
|
String name = attribute.getName();
|
|
if(usingFormat.getName().equals(Saml2ConfigurableProvider.SAML2UsingFormat.FRIENDLY_NAME.getName())){
|
|
return (friendlyName != null) ? friendlyName : name;
|
|
}
|
|
else{
|
|
return (name != null) ? name : friendlyName;
|
|
}
|
|
}
|
|
|
|
public static Object getAttributeType(XMLObject attribute, Saml2ConfigurableProvider.SAML2AttributeType attributeType){
|
|
|
|
if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSSTRING.getType())){
|
|
return ((XSString)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSINTEGER.getType())){
|
|
return ((XSInteger)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSDATETIME.getType())){
|
|
return ((XSDateTime)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSBOOLEAN.getType())){
|
|
return ((XSBoolean)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSBASE64BINARY.getType())){
|
|
return ((XSBase64Binary)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSURI.getType())){
|
|
return ((XSURI)attribute).getURI();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSQNAME.getType())){
|
|
return ((XSQName)attribute).getValue();
|
|
}
|
|
else if(attributeType.getType().equals(Saml2ConfigurableProvider.SAML2AttributeType.XSANY.getType())){
|
|
return ((XSAny)attribute).getTextContent();
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
private static XMLObject unmarshall(String saml2SSOString) throws Exception {
|
|
|
|
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
|
//documentBuilderFactory.setExpandEntityReferences(false);
|
|
documentBuilderFactory.setNamespaceAware(true);
|
|
try {
|
|
DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
|
|
ByteArrayInputStream is = new ByteArrayInputStream(saml2SSOString.getBytes(StandardCharsets.UTF_8));
|
|
Document document = docBuilder.parse(is);
|
|
Element element = document.getDocumentElement();
|
|
|
|
UnmarshallerFactory unmarshallerFactory = registry.getUnmarshallerFactory();
|
|
Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
|
|
return unmarshaller.unmarshall(element);
|
|
} catch (ParserConfigurationException | UnmarshallingException | SAXException | IOException e) {
|
|
throw new Exception("Error in unmarshalling SAML2SSO Request from the encoded String", e);
|
|
}
|
|
|
|
}
|
|
|
|
public static Response processResponse(String saml2SSOResponse, String idPEntityId, String spEntityId, String metadataUrl, boolean responseSigned, boolean assertionSigned) throws Exception {
|
|
|
|
doBootstrap();
|
|
if (saml2SSOResponse != null) {
|
|
byte[] decodedResponse = Base64.decode(saml2SSOResponse);
|
|
ByteArrayInputStream bytesIn = new ByteArrayInputStream(decodedResponse);
|
|
InflaterInputStream inflater = new InflaterInputStream(bytesIn, new Inflater(true));
|
|
String response = new BufferedReader(new InputStreamReader(inflater, StandardCharsets.UTF_8))
|
|
.lines().collect(Collectors.joining("\n"));
|
|
return processSSOResponse(response, idPEntityId, spEntityId, metadataUrl, responseSigned, assertionSigned);
|
|
|
|
} else {
|
|
throw new Exception("Invalid SAML2 Response. SAML2 Response can not be null.");
|
|
}
|
|
}
|
|
|
|
private static Response processSSOResponse(String saml2SSOResponse, String idPEntityId, String spEntityId, String metadataUrl, boolean responseSigned, boolean assertionSigned) throws Exception {
|
|
|
|
Response saml2Response = (Response) Saml2SSOUtils.unmarshall(saml2SSOResponse);
|
|
|
|
Assertion assertion = null;
|
|
// if (config.isAssertionEncrypted()) {
|
|
// List<EncryptedAssertion> encryptedAssertions = saml2Response.getEncryptedAssertions();
|
|
// EncryptedAssertion encryptedAssertion = null;
|
|
// if (!CollectionUtils.isEmpty(encryptedAssertions)) {
|
|
// encryptedAssertion = encryptedAssertions.get(0);
|
|
// try {
|
|
// assertion = getDecryptedAssertion(encryptedAssertion);
|
|
// } catch (Exception e) {
|
|
// if (log.isDebugEnabled()) {
|
|
// log.debug("Assertion decryption failure : ", e);
|
|
// }
|
|
// throw new Exception("Unable to decrypt the SAML2 Assertion");
|
|
// }
|
|
// }
|
|
// } else {
|
|
List<Assertion> assertions = saml2Response.getAssertions();
|
|
if (assertions != null && !assertions.isEmpty()) {
|
|
assertion = assertions.get(0);
|
|
}
|
|
// }
|
|
if (assertion == null) {
|
|
throw new Exception("SAML2 Assertion not found in the Response");
|
|
}
|
|
|
|
String idPEntityIdValue = assertion.getIssuer().getValue();
|
|
if (idPEntityIdValue == null || idPEntityIdValue.isEmpty()) {
|
|
throw new Exception("SAML2 Response does not contain an Issuer value");
|
|
} else if (!idPEntityIdValue.equals(idPEntityId)) {
|
|
throw new Exception("SAML2 Response Issuer verification failed");
|
|
}
|
|
|
|
String subject = null;
|
|
if (assertion.getSubject() != null && assertion.getSubject().getNameID() != null) {
|
|
subject = assertion.getSubject().getNameID().getValue();
|
|
}
|
|
|
|
if (subject == null) {
|
|
throw new Exception("SAML2 Response does not contain the name of the subject");
|
|
}
|
|
|
|
validateAudienceRestriction(assertion, spEntityId);
|
|
|
|
final HTTPMetadataResolver metadataResolver = new HTTPMetadataResolver(HttpClientBuilder.create().build(), metadataUrl);
|
|
metadataResolver.setId(metadataResolver.getClass().getCanonicalName());
|
|
metadataResolver.setParserPool(parserPool);
|
|
metadataResolver.initialize();
|
|
|
|
final MetadataCredentialResolver metadataCredentialResolver = new MetadataCredentialResolver();
|
|
final PredicateRoleDescriptorResolver roleResolver = new PredicateRoleDescriptorResolver(metadataResolver);
|
|
final KeyInfoCredentialResolver keyResolver = DefaultSecurityConfigurationBootstrap.buildBasicInlineKeyInfoCredentialResolver();
|
|
metadataCredentialResolver.setKeyInfoCredentialResolver(keyResolver);
|
|
metadataCredentialResolver.setRoleDescriptorResolver(roleResolver);
|
|
metadataCredentialResolver.initialize();
|
|
roleResolver.initialize();
|
|
|
|
CriteriaSet criteriaSet = new CriteriaSet();
|
|
criteriaSet.add(new UsageCriterion(UsageType.SIGNING));
|
|
criteriaSet.add(new EntityRoleCriterion(IDPSSODescriptor.DEFAULT_ELEMENT_NAME));
|
|
criteriaSet.add(new ProtocolCriterion(SAMLConstants.SAML20P_NS));
|
|
criteriaSet.add(new EntityIdCriterion(idPEntityId));
|
|
|
|
Credential credential = metadataCredentialResolver.resolveSingle(criteriaSet);
|
|
|
|
validateSignature(saml2Response, assertion, responseSigned, null, assertionSigned, credential);
|
|
|
|
return saml2Response;
|
|
|
|
}
|
|
|
|
private static void validateAudienceRestriction(Assertion assertion, String requiredSPEntityId) throws Exception {
|
|
|
|
if (assertion != null) {
|
|
Conditions conditions = assertion.getConditions();
|
|
if (conditions != null) {
|
|
List<AudienceRestriction> audienceRestrictions = conditions.getAudienceRestrictions();
|
|
if (audienceRestrictions != null && !audienceRestrictions.isEmpty()) {
|
|
boolean audienceFound = false;
|
|
for (AudienceRestriction audienceRestriction : audienceRestrictions) {
|
|
if (audienceRestriction.getAudiences() != null && !audienceRestriction.getAudiences().isEmpty()
|
|
) {
|
|
for (Audience audience : audienceRestriction.getAudiences()) {
|
|
if (requiredSPEntityId.equals(audience.getURI())) {
|
|
audienceFound = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (audienceFound) {
|
|
break;
|
|
}
|
|
}
|
|
if (!audienceFound) {
|
|
throw new Exception("SAML2 Assertion Audience Restriction validation failed");
|
|
}
|
|
} else {
|
|
throw new Exception("SAML2 Response doesn't contain AudienceRestrictions");
|
|
}
|
|
} else {
|
|
throw new Exception("SAML2 Response doesn't contain Conditions");
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void validateSignature(Response response, Assertion assertion, Boolean isResponseSigned, X509Credential signedResponseCredential, Boolean isAssertionSigned, Credential assertionCredential) throws Exception {
|
|
|
|
if (isResponseSigned) {
|
|
if (response.getSignature() == null) {
|
|
throw new Exception("SAML2 Response signing is enabled, but signature element not found in SAML2 Response element");
|
|
} else {
|
|
try {
|
|
SignatureValidator.validate(response.getSignature(), signedResponseCredential);
|
|
} catch (Exception e) {
|
|
throw new Exception("Signature validation failed for SAML2 Response");
|
|
}
|
|
}
|
|
}
|
|
if (isAssertionSigned) {
|
|
if (assertion.getSignature() == null) {
|
|
throw new Exception("SAML2 Assertion signing is enabled, but signature element not found in SAML2 Assertion element");
|
|
} else {
|
|
try {
|
|
SignatureValidator.validate(assertion.getSignature(), assertionCredential);
|
|
} catch (Exception e) {
|
|
throw new Exception("Signature validation failed for SAML2 Assertion");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} |