Java tutorial
/* * Copyright (c) 2016 by European Commission * * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * http://www.osor.eu/eupl/european-union-public-licence-eupl-v.1.1 * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. * */ package eu.eidas.auth.engine; import java.io.IOException; import java.io.StringWriter; import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.xml.namespace.QName; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import org.apache.commons.lang.StringUtils; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.opensaml.Configuration; import org.opensaml.common.SAMLVersion; import org.opensaml.common.SignableSAMLObject; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.common.Extensions; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Attribute; import org.opensaml.saml2.core.AttributeStatement; import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Conditions; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameIDPolicy; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.Status; import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.core.StatusMessage; import org.opensaml.saml2.core.Subject; import org.opensaml.saml2.core.SubjectConfirmation; import org.opensaml.saml2.core.SubjectConfirmationData; import org.opensaml.xml.Namespace; import org.opensaml.xml.XMLObject; import org.opensaml.xml.schema.impl.XSAnyImpl; import org.opensaml.xml.schema.impl.XSStringImpl; import org.opensaml.xml.signature.KeyInfo; import org.opensaml.xml.validation.ValidationException; import org.opensaml.xml.validation.ValidatorSuite; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import eu.eidas.auth.commons.EidasErrorKey; import eu.eidas.auth.commons.EidasStringUtil; import eu.eidas.auth.commons.attribute.AttributeDefinition; import eu.eidas.auth.commons.attribute.AttributeValue; import eu.eidas.auth.commons.attribute.AttributeValueMarshaller; import eu.eidas.auth.commons.attribute.AttributeValueMarshallingException; import eu.eidas.auth.commons.attribute.ImmutableAttributeMap; import eu.eidas.auth.commons.light.IResponseStatus; import eu.eidas.auth.commons.light.impl.ResponseStatus; import eu.eidas.auth.commons.protocol.IAuthenticationRequest; import eu.eidas.auth.commons.protocol.IAuthenticationResponse; import eu.eidas.auth.commons.protocol.IRequestMessage; import eu.eidas.auth.commons.protocol.IResponseMessage; import eu.eidas.auth.commons.protocol.impl.AuthenticationResponse; import eu.eidas.auth.commons.protocol.impl.BinaryRequestMessage; import eu.eidas.auth.commons.protocol.impl.BinaryResponseMessage; import eu.eidas.auth.engine.configuration.ConfigurationAccessor; import eu.eidas.auth.engine.configuration.FixedConfigurationAccessor; import eu.eidas.auth.engine.configuration.SamlEngineConfiguration; import eu.eidas.auth.engine.configuration.SamlEngineConfigurationException; import eu.eidas.auth.engine.configuration.dom.DefaultConfigurationAccessor; import eu.eidas.auth.engine.configuration.dom.DefaultSamlEngineConfigurationFactory; import eu.eidas.auth.engine.configuration.dom.SamlEngineConfigurationFactory; import eu.eidas.auth.engine.core.ExtensionProcessorI; import eu.eidas.auth.engine.core.SAMLExtensionFormat; import eu.eidas.auth.engine.core.eidas.GenericEidasAttributeType; import eu.eidas.auth.engine.xml.opensaml.AssertionUtil; import eu.eidas.auth.engine.xml.opensaml.BuilderFactoryUtil; import eu.eidas.auth.engine.xml.opensaml.CertificateUtil; import eu.eidas.auth.engine.xml.opensaml.SAMLEngineUtils; import eu.eidas.auth.engine.xml.opensaml.XmlSchemaUtil; import eu.eidas.engine.exceptions.EIDASSAMLEngineException; import eu.eidas.engine.exceptions.EIDASSAMLEngineRuntimeException; import eu.eidas.samlengineconfig.CertificateConfigurationManager; import eu.eidas.util.Preconditions; /** * Due to business constraints, this class is part of the contract with a DG Taxud project, so keep it as is. * <p> * Remove this class in 1.2. * * @deprecated since 1.1, use {@link ProtocolEngine} instead. */ @Deprecated @Beta public final class SamlEngine extends AbstractSamlEngine implements SamlEngineI { private static final class LazyDefaultSamlEngines { private static final ImmutableMap<String, SamlEngineI> DEFAULT_SAML_ENGINES; static { SamlEngineConfigurationFactory configurationFactory = DefaultSamlEngineConfigurationFactory .getInstance(); ImmutableMap.Builder<String, SamlEngineI> builder = ImmutableMap.builder(); ImmutableMap<String, SamlEngineConfiguration> map; try { map = configurationFactory.getConfigurationMapAccessor().get(); } catch (IOException e) { throw new IllegalStateException(e); } for (final String instanceName : map.keySet()) { builder.put(instanceName, createDefaultSamlEngine(instanceName)); } DEFAULT_SAML_ENGINES = builder.build(); } @Nonnull private static SamlEngineI createDefaultSamlEngine(@Nonnull String instanceName) { return new SamlEngine(new DefaultConfigurationAccessor(instanceName)); } } /** * The Constant LOG. */ private static final Logger LOG = LoggerFactory.getLogger(SamlEngine.class); public static final String ATTRIBUTE_EMPTY_LITERAL = "Attribute name is null or empty."; /** * Creates an instance of SamlEngine. * * @param nameInstance the name instance * @return instance of SamlEngine */ @Nonnull public static SamlEngineI createSAMLEngine(@Nonnull String nameInstance, @Nonnull ExtensionProcessorI extensionProcessor) throws EIDASSAMLEngineException { return createSAMLEngine(nameInstance, null, extensionProcessor, new SamlEngineSystemClock()); } /** * Returns a default SamlEngine instance matching the given name retrieved from the configuration file. * * @param instanceName the instance name * @return the SamlEngine instance matching the given name retrieved from the configuration file */ @Nullable public static SamlEngineI getDefaultSamlEngine(@Nonnull String instanceName) { Preconditions.checkNotBlank(instanceName, "instanceName"); return LazyDefaultSamlEngines.DEFAULT_SAML_ENGINES.get(instanceName.trim()); } @Nonnull public static SamlEngineI createSAMLEngine(@Nonnull String nameInstance, CertificateConfigurationManager configManager, @Nonnull ExtensionProcessorI extensionProcessor, @Nonnull SamlEngineClock samlEngineClock) throws SamlEngineConfigurationException { LOG.info(SAML_EXCHANGE, "create instance: {} ", nameInstance); SamlEngineConfiguration samlEngineConfiguration = SamlEngineConfigurationFactory .getConfigurationMap(configManager).get(nameInstance); extensionProcessor.configureExtension(); SamlEngineConfiguration configuration = SamlEngineConfiguration.builder(samlEngineConfiguration) .extensionProcessor(extensionProcessor).clock(samlEngineClock).build(); SamlEngineI samlEngine = new SamlEngine(new FixedConfigurationAccessor(configuration)); LOG.info(SAML_EXCHANGE, "created instance: {} ", samlEngine); return samlEngine; } /** * Constructs a new Saml engine instance. * * @param configurationAccessor the accessor to the configuration of this instance. */ public SamlEngine(@Nonnull ConfigurationAccessor configurationAccessor) { super(configurationAccessor); } /** * Generate authentication response base. * * @param status the status * @param assertConsumerURL the assert consumer URL. * @param inResponseTo the in response to * @return the response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private Response genAuthnRespBase(Status status, String assertConsumerURL, String inResponseTo) throws EIDASSAMLEngineException { LOG.debug("Generate Authentication Response base."); Response response = BuilderFactoryUtil.generateResponse(SAMLEngineUtils.generateNCName(), SAMLEngineUtils.getCurrentTime(), status); // Set name Spaces registerResponseNamespace(response); // Mandatory EIDAS LOG.debug("Generate Issuer"); Issuer issuer = BuilderFactoryUtil.generateIssuer(); issuer.setValue(getCoreProperties().getResponder()); // Format Entity Optional EIDAS issuer.setFormat(getCoreProperties().getFormatEntity()); response.setIssuer(issuer); // destination Mandatory EIDAS if (assertConsumerURL != null) { response.setDestination(assertConsumerURL.trim()); } // inResponseTo Mandatory response.setInResponseTo(inResponseTo.trim()); // Optional response.setConsent(getCoreProperties().getConsentAuthnResponse()); return response; } @Nonnull private AttributeDefinition getAttributeDefinitionNotNull(@Nonnull String name) throws EIDASSAMLEngineException { AttributeDefinition attributeDefinition = getExtensionProcessor().getAttributeDefinitionNullable(name); if (null == attributeDefinition) { LOG.info("BUSINESS EXCEPTION : Attribute name: {} is not known.", name); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), "Attribute name: " + name + " is not known."); } return attributeDefinition; } /** * Generates one attribute statement for the response. * * @param attributeMap the personal attribute map * @return the attribute statement for the response * @throws EIDASSAMLEngineException the SAML engine exception */ private AttributeStatement generateResponseAttributeStatement(@Nonnull ImmutableAttributeMap attributeMap) throws EIDASSAMLEngineException { LOG.trace("Generate attribute statement"); AttributeStatement attrStatement = (AttributeStatement) BuilderFactoryUtil .buildXmlObject(AttributeStatement.DEFAULT_ELEMENT_NAME); List<Attribute> list = attrStatement.getAttributes(); for (final Map.Entry<AttributeDefinition<?>, ImmutableSet<? extends AttributeValue<?>>> entry : attributeMap .getAttributeMap().entrySet()) { // Verification that only one value is permitted, simple or // complex, not both. ImmutableSet<? extends AttributeValue<?>> value = entry.getValue(); addToAttributeList(list, entry.getKey(), value); } return attrStatement; } private void addToAttributeList(@Nonnull List<Attribute> list, @Nonnull AttributeDefinition<?> attributeDefinition, @Nonnull ImmutableSet<? extends AttributeValue<?>> values) throws EIDASSAMLEngineException { // TODO take transliteration into account AttributeValueMarshaller<?> attributeValueMarshaller = attributeDefinition.getAttributeValueMarshaller(); ImmutableSet.Builder<String> builder = ImmutableSet.builder(); for (final AttributeValue<?> attributeValue : values) { try { String marshalledValue = attributeValueMarshaller.marshal((AttributeValue) attributeValue); builder.add(marshalledValue); } catch (AttributeValueMarshallingException e) { LOG.error("Illegal attribute value: " + e, e); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), e); } } list.add(getExtensionProcessor().generateAttrSimple(attributeDefinition, builder.build())); } private static final CharsetEncoder LATIN_1_CHARSET_ENCODER = Charset.forName("ISO-8859-1").newEncoder(); public static boolean needsTransliteration(String v) { return !LATIN_1_CHARSET_ENCODER.canEncode(v); } @Nullable private AttributeStatement findAttributeStatementNullable(@Nonnull Assertion assertion) throws EIDASSAMLEngineException { List<XMLObject> orderedChildren = assertion.getOrderedChildren(); // Search the attribute statement. for (XMLObject child : orderedChildren) { if (child instanceof AttributeStatement) { return (AttributeStatement) child; } } return null; } @Nonnull private AttributeStatement findAttributeStatement(@Nonnull Assertion assertion) throws EIDASSAMLEngineException { AttributeStatement attributeStatement = findAttributeStatementNullable(assertion); if (null != attributeStatement) { return attributeStatement; } LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : AttributeStatement not present."); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), "AttributeStatement not present."); } private String computeSimpleValue(XSAnyImpl xsAny) { if (null != xsAny) { List<XMLObject> unknownXMLObjects = xsAny.getUnknownXMLObjects(); if (null != unknownXMLObjects && !unknownXMLObjects.isEmpty()) { try { TransformerFactory transFactory = TransformerFactory.newInstance(); Transformer transformer = transFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); StringWriter stringWriter = new StringWriter(); transformer.transform(new DOMSource(unknownXMLObjects.get(0).getDOM()), new StreamResult(stringWriter)); return stringWriter.toString(); } catch (TransformerConfigurationException e) { LOG.warn(SAML_EXCHANGE, "ERROR : transformer configuration exception", e); } catch (TransformerException e) { LOG.warn(SAML_EXCHANGE, "ERROR : transformer exception", e); } } return xsAny.getTextContent(); } return null; } private Map<String, String> computeComplexValue(XSAnyImpl complexValue) { Map<String, String> multiValues = new HashMap<String, String>(); for (final XMLObject xmlObject : complexValue.getUnknownXMLObjects()) { XSAnyImpl simple = (XSAnyImpl) xmlObject; multiValues.put(simple.getElementQName().getLocalPart(), simple.getTextContent()); } return multiValues; } /** * Converts an assertion to an attribute map. * * @param assertion the assertion * @return the attribute map * @throws EIDASSAMLEngineException the SAML engine exception */ @Nonnull private ImmutableAttributeMap convertToAttributeMap(@Nonnull Assertion assertion) throws EIDASSAMLEngineException { LOG.trace("Generate personal attribute list from XMLObject."); AttributeStatement attributeStatement = findAttributeStatement(assertion); List<Attribute> attributes = attributeStatement.getAttributes(); ImmutableAttributeMap.Builder mapBuilder = ImmutableAttributeMap.builder(); // Process the attributes. for (final Attribute attribute : attributes) { String attributeName = attribute.getName(); String friendlyName = attribute.getFriendlyName(); AttributeDefinition<?> attributeDefinition = getAttributeDefinitionNotNull(attributeName); AttributeValueMarshaller<?> attributeValueMarshaller = attributeDefinition .getAttributeValueMarshaller(); ImmutableSet.Builder<AttributeValue<?>> setBuilder = new ImmutableSet.Builder<>(); List<XMLObject> values = attribute.getOrderedChildren(); QName xmlType = attributeDefinition.getXmlType(); QName latinScript = new QName(xmlType.getNamespaceURI(), "LatinScript", xmlType.getPrefix()); // Process the values. for (XMLObject xmlObject : values) { try { if (xmlObject instanceof XSStringImpl) { // Process simple value. setBuilder.add( attributeValueMarshaller.unmarshal(((XSStringImpl) xmlObject).getValue(), false)); } else if (xmlObject instanceof XSAnyImpl) { XSAnyImpl xsAny = (XSAnyImpl) xmlObject; // TODO: move to STORK Extension Processor if ("http://www.stork.gov.eu/1.0/signedDoc".equals(attributeName)) { setBuilder.add(attributeValueMarshaller.unmarshal(computeSimpleValue(xsAny), false)); // TODO: move to STORK Extension Processor } else if ("http://www.stork.gov.eu/1.0/canonicalResidenceAddress".equals(attributeName)) { // Process complex value. setBuilder.add(attributeValueMarshaller.unmarshal(computeComplexValue(xsAny).toString(), false)); } else { boolean isNonLatinScriptAlternateVersion = false; String latinScriptAttrValue = xsAny.getUnknownAttributes().get(latinScript); if (StringUtils.isNotBlank(latinScriptAttrValue) && "false".equals(latinScriptAttrValue)) { isNonLatinScriptAlternateVersion = true; } // Process simple value. setBuilder.add(attributeValueMarshaller.unmarshal(xsAny.getTextContent(), isNonLatinScriptAlternateVersion)); } // TODO: remove } else if (xmlObject instanceof GenericEidasAttributeType) { // Process simple value. setBuilder.add(attributeValueMarshaller .unmarshal(((GenericEidasAttributeType) xmlObject).getValue(), false)); } else { LOG.info( "BUSINESS EXCEPTION : attribute value is unknown in generatePersonalAttributeList."); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), "Attribute value is unknown for \"" + attributeDefinition.getNameUri().toASCIIString() + "\" - value: \"" + xmlObject + "\""); } } catch (AttributeValueMarshallingException e) { LOG.error("BUSINESS EXCEPTION : Illegal Attribute Value: " + e, e); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), e); } } // Check if friendlyName matches when provided - TODO Temorary removed due to validator failure /* if (StringUtils.isNotEmpty(friendlyName) && attributeDefinition != null && !friendlyName.equals(attributeDefinition.getFriendlyName())) { LOG.error("BUSINESS EXCEPTION : Illegal Attribute friendlyName for " + attributeDefinition.getNameUri().toString() + " expected " + attributeDefinition.getFriendlyName() + " got " + friendlyName); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), "Illegal Attribute friendlyName for " + attributeDefinition.getNameUri().toString() + " expected " + attributeDefinition.getFriendlyName() + " got " + friendlyName); }*/ mapBuilder.put((AttributeDefinition) attributeDefinition, (ImmutableSet) setBuilder.build()); } return mapBuilder.build(); } /** * Generate the authentication request. * * @param request the request that contain all parameters for generate an authentication request. * @return the EIDAS authentication request that has been processed. * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IRequestMessage generateRequestMessage(IAuthenticationRequest request) throws EIDASSAMLEngineException { return generateRequestMessage(request, true, true); } private IRequestMessage generateRequestMessage(IAuthenticationRequest request, boolean validate, boolean sign) throws EIDASSAMLEngineException { LOG.trace("Generate SAMLAuthnRequest."); if (null == request) { LOG.debug(SAML_EXCHANGE, "Sign and Marshall - null input"); LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Sign and Marshall -null input"); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage()); } if (validate) { // Validate mandatory parameters getExtensionProcessor().validateAuthenticationRequest(request, null); } String id = SAMLEngineUtils.generateNCName(); AuthnRequest authnRequestAux = BuilderFactoryUtil.generateAuthnRequest(id, SAMLVersion.VERSION_20, SAMLEngineUtils.getCurrentTime()); // Set name spaces. registerRequestNamespace(authnRequestAux); // Add parameter Mandatory authnRequestAux.setForceAuthn(Boolean.TRUE); // Add parameter Mandatory authnRequestAux.setIsPassive(Boolean.FALSE); authnRequestAux.setAssertionConsumerServiceURL(request.getAssertionConsumerServiceURL()); authnRequestAux.setProviderName(request.getProviderName()); // Add protocol binding authnRequestAux .setProtocolBinding(getExtensionProcessor().getProtocolBinding(request, getCoreProperties())); // Add parameter optional // Destination is mandatory // The application must to know the destination if (StringUtils.isNotBlank(request.getDestination())) { authnRequestAux.setDestination(request.getDestination()); } // Consent is optional. Set from SAMLEngine.xml - consent. authnRequestAux.setConsent(getCoreProperties().getConsentAuthnRequest()); Issuer issuer = BuilderFactoryUtil.generateIssuer(); if (request.getIssuer() != null) { issuer.setValue(SAMLEngineUtils.getValidIssuerValue(request.getIssuer())); } else { issuer.setValue(getCoreProperties().getRequester()); } // Optional String formatEntity = getCoreProperties().getFormatEntity(); if (StringUtils.isNotBlank(formatEntity)) { issuer.setFormat(formatEntity); } authnRequestAux.setIssuer(issuer); getExtensionProcessor().addRequestedAuthnContext(request, authnRequestAux); // Generate format extensions. Extensions formatExtensions = getExtensionProcessor().generateExtensions(getCoreProperties(), request); // add the extensions to the SAMLAuthnRequest authnRequestAux.setExtensions(formatExtensions); addNameIDPolicy(authnRequestAux, request.getNameIdFormat()); // the result contains an authentication request token (byte[]), // identifier of the token, and all parameters from the request. IAuthenticationRequest authRequestFromExtensionProcessor = getExtensionProcessor() .processExtensions(request.getCitizenCountryCode(), authnRequestAux, null, null); byte[] bytes; try { if (sign) { bytes = signAndMarshallRequest(authnRequestAux); } else { bytes = noSignAndMarshall(authnRequestAux); } } catch (EIDASSAMLEngineException e) { LOG.debug(SAML_EXCHANGE, "Sign and Marshall.", e); LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Sign and Marshall.", e); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } return new BinaryRequestMessage(authRequestFromExtensionProcessor, bytes); } private void addNameIDPolicy(AuthnRequest authnRequestAux, String selectedNameID) throws EIDASSAMLEngineException { if (getExtensionProcessor().getFormat() == SAMLExtensionFormat.EIDAS10 && !StringUtils.isEmpty(selectedNameID)) { NameIDPolicy policy = (NameIDPolicy) BuilderFactoryUtil .buildXmlObject(NameIDPolicy.DEFAULT_ELEMENT_NAME); policy.setFormat(selectedNameID); policy.setAllowCreate(true); authnRequestAux.setNameIDPolicy(policy); } } /** * Generate authentication response in one of the supported formats. * * @param request the request * @param response the response authentication request * @param ipAddress the IP address * @return the authentication response * @throws EIDASSAMLEngineException the EIDASSAML engine exception * @deprecated this method is only used in unit tests, do not use it as it does not sign the response */ @Override @Deprecated @VisibleForTesting public IResponseMessage generateResponseMessage(IAuthenticationRequest request, IAuthenticationResponse response, String ipAddress) throws EIDASSAMLEngineException { return generateResponseMessage(request, response, false, ipAddress); } /** * Generate authentication response in one of the supported formats. * * @param request the request * @param authnResponse the authentication response from the IdP * @param ipAddress the IP address * @param signAssertion whether to sign the attribute assertion * @return the authentication response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IResponseMessage generateResponseMessage(IAuthenticationRequest request, IAuthenticationResponse authnResponse, boolean signAssertion, String ipAddress) throws EIDASSAMLEngineException { LOG.trace("generateResponseMessage"); // Validate parameters validateParamResponse(request, authnResponse); // At this point the assertion consumer service URL is mandatory (and must have been replaced by the value from the metadata if needed) if (StringUtils.isBlank(request.getAssertionConsumerServiceURL())) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Request AssertionConsumerServiceURL must not be blank."); } // Mandatory SAML LOG.trace("Generate StatusCode"); StatusCode statusCode = BuilderFactoryUtil.generateStatusCode(StatusCode.SUCCESS_URI); LOG.trace("Generate Status"); Status status = BuilderFactoryUtil.generateStatus(statusCode); LOG.trace("Generate StatusMessage"); StatusMessage statusMessage = BuilderFactoryUtil.generateStatusMessage(StatusCode.SUCCESS_URI); status.setStatusMessage(statusMessage); LOG.trace("Generate Response"); // RESPONSE Response response = genAuthnRespBase(status, request.getAssertionConsumerServiceURL(), request.getId()); if (authnResponse.getIssuer() != null && !authnResponse.getIssuer().isEmpty() && response.getIssuer() != null) { response.getIssuer().setValue(SAMLEngineUtils.getValidIssuerValue(authnResponse.getIssuer())); } DateTime notOnOrAfter = new DateTime(); notOnOrAfter = notOnOrAfter.plusSeconds(getCoreProperties().getTimeNotOnOrAfter()); Assertion assertion = AssertionUtil.generateResponseAssertion(false, ipAddress, request, response.getIssuer(), authnResponse.getAttributes(), notOnOrAfter, getCoreProperties().getFormatEntity(), getCoreProperties().getResponder(), getExtensionProcessor().getFormat(), getCoreProperties().isOneTimeUse()); AttributeStatement attrStatement = generateResponseAttributeStatement(authnResponse.getAttributes()); assertion.getAttributeStatements().add(attrStatement); addResponseAuthnContextClassRef(authnResponse, assertion); // Add assertions Assertion signedAssertion = null; if (signAssertion) { try { signedAssertion = (Assertion) signAssertion(assertion); } catch (EIDASSAMLEngineException exc) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : cannot sign assertion: {}", exc.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : cannot sign assertion: {}", exc); } } response.getAssertions().add(signedAssertion == null ? assertion : signedAssertion); try { byte[] responseBytes = signAndMarshallResponse(request, response); return new BinaryResponseMessage(authnResponse, responseBytes); } catch (EIDASSAMLEngineException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Sign and Marshall.", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : Sign and Marshall.", e); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } } // TODO move this to ExtensionProcessors private void addResponseAuthnContextClassRef(IAuthenticationResponse responseAuthReq, Assertion assertion) throws EIDASSAMLEngineException { if (!StringUtils.isEmpty(responseAuthReq.getLevelOfAssurance())) { AuthnContextClassRef authnContextClassRef = assertion.getAuthnStatements().get(0).getAuthnContext() .getAuthnContextClassRef(); if (authnContextClassRef == null) { authnContextClassRef = (AuthnContextClassRef) BuilderFactoryUtil .buildXmlObject(AuthnContextClassRef.DEFAULT_ELEMENT_NAME); assertion.getAuthnStatements().get(0).getAuthnContext() .setAuthnContextClassRef(authnContextClassRef); } authnContextClassRef.setAuthnContextClassRef(responseAuthReq.getLevelOfAssurance()); } } /** * Generate authentication response fail. * * @param request the request * @param response the response * @param ipAddress the IP address * @return the authentication response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IResponseMessage generateResponseMessageFail(IAuthenticationRequest request, IAuthenticationResponse response, String ipAddress) throws EIDASSAMLEngineException { LOG.trace("generateResponseMessageFail"); validateParamResponseFail(request, response); // Mandatory StatusCode statusCode = BuilderFactoryUtil.generateStatusCode(response.getStatusCode()); // Mandatory SAML LOG.trace("Generate StatusCode."); // Subordinate code is optional in case not covered into next codes: // - urn:oasis:names:tc:SAML:2.0:status:AuthnFailed // - urn:oasis:names:tc:SAML:2.0:status:InvalidAttrNameOrValue // - urn:oasis:names:tc:SAML:2.0:status:InvalidNameIDPolicy // - urn:oasis:names:tc:SAML:2.0:status:RequestDenied // - http://www.stork.gov.eu/saml20/statusCodes/QAANotSupported if (StringUtils.isNotBlank(response.getSubStatusCode())) { StatusCode newStatusCode = BuilderFactoryUtil.generateStatusCode(response.getSubStatusCode()); statusCode.setStatusCode(newStatusCode); } LOG.debug("Generate Status."); Status status = BuilderFactoryUtil.generateStatus(statusCode); if (StringUtils.isNotBlank(response.getStatusMessage())) { StatusMessage statusMessage = BuilderFactoryUtil.generateStatusMessage(response.getStatusMessage()); status.setStatusMessage(statusMessage); } LOG.trace("Generate Response."); // RESPONSE Response responseFail = genAuthnRespBase(status, request.getAssertionConsumerServiceURL(), request.getId()); String responseIssuer = response.getIssuer(); if (responseIssuer != null && !responseIssuer.isEmpty()) { responseFail.getIssuer().setValue(responseIssuer); } DateTime notOnOrAfter = new DateTime(); notOnOrAfter = notOnOrAfter.plusSeconds(getCoreProperties().getTimeNotOnOrAfter()); Assertion assertion = AssertionUtil.generateResponseAssertion(true, ipAddress, request, responseFail.getIssuer(), ImmutableAttributeMap.of(), notOnOrAfter, getCoreProperties().getFormatEntity(), getCoreProperties().getResponder(), getExtensionProcessor().getFormat(), getCoreProperties().isOneTimeUse()); addResponseAuthnContextClassRef(response, assertion); responseFail.getAssertions().add(assertion); LOG.trace("Sign and Marshall ResponseFail."); AuthenticationResponse.Builder eidasResponse = new AuthenticationResponse.Builder(); try { byte[] responseBytes = signAndMarshallResponse(request, responseFail); eidasResponse.id(responseFail.getID()); eidasResponse.issuer(responseFail.getIssuer().getValue()); eidasResponse.ipAddress(ipAddress); eidasResponse.inResponseTo(responseFail.getInResponseTo()); eidasResponse.responseStatus(extractResponseStatus(responseFail)); return new BinaryResponseMessage(eidasResponse.build(), responseBytes); } catch (EIDASSAMLEngineException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : SAMLEngineException.", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : SAMLEngineException.", e); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } } /** * Gets the country from X.509 Certificate. * * @param keyInfo the key info * @return the country */ private String getCountry(KeyInfo keyInfo) { LOG.trace("Recover country information."); try { org.opensaml.xml.signature.X509Certificate xmlCert = keyInfo.getX509Datas().get(0).getX509Certificates() .get(0); // Transform the KeyInfo to X509Certificate. X509Certificate cert = CertificateUtil.toCertificate(xmlCert.getValue()); String distName = cert.getSubjectDN().toString(); distName = StringUtils.deleteWhitespace(StringUtils.upperCase(distName)); String countryCode = "C="; int init = distName.indexOf(countryCode); String result = ""; if (init > StringUtils.INDEX_NOT_FOUND) { // Exist country code. int end = distName.indexOf(',', init); if (end <= StringUtils.INDEX_NOT_FOUND) { end = distName.length(); } if (init < end && end > StringUtils.INDEX_NOT_FOUND) { result = distName.substring(init + countryCode.length(), end); //It must be a two characters value if (result.length() > 2) { result = result.substring(0, 2); } } } return result.trim(); } catch (EIDASSAMLEngineException e) { LOG.error(SAML_EXCHANGE, "BUSINESS EXCEPTION : Procces getCountry from certificate: " + e.getMessage(), e); throw new EIDASSAMLEngineRuntimeException(e); } } /** * Sets the name spaces. * * @param xmlToken the new name spaces */ private void registerRequestNamespace(@Nonnull XMLObject xmlToken) { LOG.trace("Set namespaces."); xmlToken.getNamespaceManager() .registerNamespace(new Namespace(SAMLConstants.SAML20_NS, SAMLConstants.SAML20_PREFIX)); xmlToken.getNamespaceManager().registerNamespace(new Namespace("http://www.w3.org/2000/09/xmldsig#", "ds")); xmlToken.getNamespaceManager() .registerNamespace(new Namespace(SAMLConstants.SAML20P_NS, SAMLConstants.SAML20P_PREFIX)); // Calling the extension processor extension getExtensionProcessor().registerRequestNamespace(xmlToken); } /** * Register the namespace on the response SAML xml token * * @param xmlToken */ void registerResponseNamespace(@Nonnull XMLObject xmlToken) { LOG.trace("Set namespaces."); xmlToken.getNamespaceManager() .registerNamespace(new Namespace(SAMLConstants.SAML20_NS, SAMLConstants.SAML20_PREFIX)); xmlToken.getNamespaceManager().registerNamespace(new Namespace("http://www.w3.org/2000/09/xmldsig#", "ds")); xmlToken.getNamespaceManager() .registerNamespace(new Namespace(SAMLConstants.SAML20P_NS, SAMLConstants.SAML20P_PREFIX)); // Calling the extension processor extension getExtensionProcessor().registerResponseNamespace(xmlToken); } /** * Validate parameters from response. * * @param request the request * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private void checkRequestSanity(IAuthenticationRequest request) throws EIDASSAMLEngineException { getExtensionProcessor().checkRequestSanity(request); } /** * Validate parameters from response. * * @param response the response authentication request * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private void checkResponseSanity(IAuthenticationResponse response) throws EIDASSAMLEngineException { if (response.getAttributes() == null || response.getAttributes().isEmpty()) { LOG.error(SAML_EXCHANGE, "No attribute values in response."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "No attribute values in response."); } } /** * Validate parameters from response. * * @param request the request * @param response the response authentication request * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private void validateParamResponse(IAuthenticationRequest request, IAuthenticationResponse response) throws EIDASSAMLEngineException { LOG.trace("Validate parameters response."); checkRequestSanity(request); checkResponseSanity(response); } /** * Validate parameter from response fail. * * @param request the request * @param response the response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private void validateParamResponseFail(IAuthenticationRequest request, IAuthenticationResponse response) throws EIDASSAMLEngineException { LOG.trace("Validate parameters response fail."); if (StringUtils.isBlank(response.getStatusCode())) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Error Status Code is null or empty."); } if (StringUtils.isBlank(request.getAssertionConsumerServiceURL())) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "assertionConsumerServiceURL is null or empty."); } if (StringUtils.isBlank(request.getId())) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "request ID is null or empty."); } } /** * Process and validates the authentication request. * * @param tokenSaml the token SAML * @return the authentication request * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IAuthenticationRequest processValidateRequestToken(@Nonnull String citizenCountryCode, byte[] tokenSaml) throws EIDASSAMLEngineException { LOG.trace("processValidateRequestToken"); if (tokenSaml == null) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Saml authentication request is null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Saml authentication request is null."); } XmlSchemaUtil.validateSamlSchema(EidasStringUtil.toString(tokenSaml)); AuthnRequest originalSamlRequest = validateRequestHelper(tokenSaml); LOG.trace("Generate EIDASAuthnSamlRequest."); String originCountryCode = (originalSamlRequest.getSignature() != null) ? getCountry(originalSamlRequest.getSignature().getKeyInfo()) : null; IAuthenticationRequest authRequestFromExtensionProcessor = getExtensionProcessor() .unmarshallRequest(citizenCountryCode, originalSamlRequest, originCountryCode); checkRequestSanity(authRequestFromExtensionProcessor); return authRequestFromExtensionProcessor; } private AuthnRequest validateRequestHelper(byte[] tokenSaml) throws EIDASSAMLEngineException { LOG.trace("Validate AuthnRequest"); AuthnRequest samlRequest; try { samlRequest = validateRequestWithSuite(tokenSaml); } catch (ValidationException e) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorMessage(), e); } if (samlRequest == null) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorMessage()); } return samlRequest; } private AuthnRequest validateRequestWithSuite(byte[] requestBytes) throws EIDASSAMLEngineException, ValidationException { ExtensionProcessorI extensionProcessor = getExtensionProcessor(); AuthnRequest samlRequest; ValidatorSuite suite = Configuration.getValidatorSuite(extensionProcessor.getRequestValidatorId()); samlRequest = validateRequestBytes(requestBytes); try { suite.validate(samlRequest); if (tryProcessExtensions(extensionProcessor, samlRequest)) { LOG.debug("validation with " + extensionProcessor.getClass().getName() + " succeeded !!!"); } else { LOG.debug("validation with " + extensionProcessor.getClass().getName() + " tryProcessExtensions() returned false"); samlRequest = null; } } catch (ValidationException e) { LOG.debug("validation with " + extensionProcessor.getClass().getName() + " failed: " + e, e); throw e; } return samlRequest; } private Response computeAuxResponse(byte[] responseBytes) throws EIDASSAMLEngineException { Response samlResponseAux = null; try { samlResponseAux = validateResponseBytes(responseBytes); if (decryptResponse()) { /* In the @eu.eidas.encryption.SAMLAuthnResponseDecrypter.decryptSAMLResponse method when inserting the decrypted Assertions the DOM resets to null. Marsahlling it again resolves it. More info in the links belows https://jira.spring.io/browse/SES-148 http://digitaliser.dk/forum/2621692 */ noSignAndMarshall(samlResponseAux); } } catch (EIDASSAMLEngineException e) { LOG.warn("error validating the response ", e.getMessage()); LOG.debug("error validating the response", e); } return samlResponseAux; } private void validateSamlResponse(Response samlResponse) throws EIDASSAMLEngineException { LOG.trace("Validate AuthnResponse"); ValidatorSuite suite = Configuration.getValidatorSuite(getExtensionProcessor().getResponseValidatorId()); try { suite.validate(samlResponse); } catch (ValidationException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException: validate AuthResponse.", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException: validate AuthResponse.", e); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorMessage(), e); } catch (Exception e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException: validate AuthResponse.", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException: validate AuthResponse.", e); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorMessage(), e); } } private IResponseStatus extractResponseStatus(@Nonnull Response samlResponse) { ResponseStatus.Builder builder = ResponseStatus.builder(); LOG.trace("Set statusCode."); Status status = samlResponse.getStatus(); StatusCode statusCode = status.getStatusCode(); String statusCodeValue = statusCode.getValue(); builder.statusCode(statusCodeValue); builder.failure(isFailureStatusCode(statusCodeValue)); // Subordinate code. StatusCode subStatusCode = statusCode.getStatusCode(); if (subStatusCode != null) { builder.subStatusCode(subStatusCode.getValue()); } if (status.getStatusMessage() != null) { LOG.trace("Set statusMessage."); builder.statusMessage(status.getStatusMessage().getMessage()); } return builder.build(); } private boolean isFailureStatusCode(String statusCodeValue) { return !StatusCode.SUCCESS_URI.equals(statusCodeValue); } private AuthenticationResponse.Builder createResponseBuilder(Response samlResponse) { LOG.trace("Create EidasAuthResponse."); AuthenticationResponse.Builder responseBuilder = new AuthenticationResponse.Builder(); responseBuilder.country(getCountry(samlResponse.getSignature().getKeyInfo())); LOG.trace("Set ID."); responseBuilder.id(samlResponse.getID()); LOG.trace("Set InResponseTo."); responseBuilder.inResponseTo(samlResponse.getInResponseTo()); responseBuilder.issuer(samlResponse.getIssuer().getValue()); responseBuilder.encrypted( samlResponse.getEncryptedAssertions() != null && !samlResponse.getEncryptedAssertions().isEmpty()); return responseBuilder; } /** * Marshalls the given bytes into a SAML Response. * * @param tokenSaml the SAML response bytes * @return the SAML response instance * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public Response marshall(@Nonnull byte[] tokenSaml) throws EIDASSAMLEngineException { LOG.trace("processValidateResponseToken"); if (null == tokenSaml) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Saml authentication response is null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Saml authentication response is null."); } XmlSchemaUtil.validateSamlSchema(EidasStringUtil.toString(tokenSaml)); Response samlResponse = computeAuxResponse(tokenSaml); validateSamlResponse(samlResponse); return samlResponse; } /** * Process and validates the authentication response. * * @param responseBytes the token SAML * @param userIP the user IP * @return the authentication response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IAuthenticationResponse processValidateResponseToken(byte[] responseBytes, String userIP, long skewTimeInMillis) throws EIDASSAMLEngineException { Response samlResponse = marshall(responseBytes); return validateMarshalledResponse(samlResponse, userIP, skewTimeInMillis); } /** * Validate authentication response. * * @param samlResponse the token SAML * @param userIP the user IP * @param skewTimeInMillis the skew time * @return the authentication response * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ @Override public IAuthenticationResponse validateMarshalledResponse(Response samlResponse, String userIP, long skewTimeInMillis) throws EIDASSAMLEngineException { AuthenticationResponse.Builder authnResponse = createResponseBuilder(samlResponse); IResponseStatus responseStatus = extractResponseStatus(samlResponse); authnResponse.responseStatus(responseStatus); LOG.trace("validateEidasResponse"); Assertion assertion = validateResponse(samlResponse, userIP, skewTimeInMillis); if (assertion != null) { LOG.trace("Set notOnOrAfter."); authnResponse.notOnOrAfter(assertion.getConditions().getNotOnOrAfter()); LOG.trace("Set notBefore."); authnResponse.notBefore(assertion.getConditions().getNotBefore()); authnResponse.audienceRestriction((assertion.getConditions().getAudienceRestrictions().get(0)) .getAudiences().get(0).getAudienceURI()); if (!assertion.getAuthnStatements().isEmpty() && assertion.getAuthnStatements().get(0).getAuthnContext() != null && assertion.getAuthnStatements().get(0).getAuthnContext().getAuthnContextClassRef() != null) { authnResponse.levelOfAssurance(assertion.getAuthnStatements().get(0).getAuthnContext() .getAuthnContextClassRef().getAuthnContextClassRef()); } } // Case no error. if (assertion != null && !isFailure(responseStatus)) { LOG.trace("Status Success. Set PersonalAttributeList."); authnResponse.attributes(convertToAttributeMap(assertion)); } else { LOG.trace("Status Fail."); } LOG.trace("Return result."); return authnResponse.build(); } private boolean isFailure(@Nonnull IResponseStatus responseStatus) { return responseStatus.isFailure() || isFailureStatusCode(responseStatus.getStatusCode()); } /** * Validate response. * * @param samlResponse the SAML response * @param userIP the user IP * @return the assertion * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private Assertion validateResponse(Response samlResponse, String userIP, Long skewTimeInMillis) throws EIDASSAMLEngineException { // Exist only one Assertion if (samlResponse.getAssertions() == null || samlResponse.getAssertions().isEmpty()) { //in replace of throwing EIDASSAMLEngineException("Assertion is null or empty.") LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Assertion is null, empty or the response is encrypted and the decryption is not active."); return null; } Assertion assertion = (Assertion) samlResponse.getAssertions().get(0); verifyMethodBearer(userIP, assertion); // Applying skew time conditions before testing it DateTime skewedNotBefore = new DateTime( assertion.getConditions().getNotBefore().getMillis() - skewTimeInMillis, DateTimeZone.UTC); DateTime skewedNotOnOrAfter = new DateTime( assertion.getConditions().getNotOnOrAfter().getMillis() + skewTimeInMillis, DateTimeZone.UTC); LOG.debug(SAML_EXCHANGE, "skewTimeInMillis : {}", skewTimeInMillis); LOG.debug(SAML_EXCHANGE, "skewedNotBefore : {}", skewedNotBefore); LOG.debug(SAML_EXCHANGE, "skewedNotOnOrAfter : {}", skewedNotOnOrAfter); assertion.getConditions().setNotBefore(skewedNotBefore); assertion.getConditions().setNotOnOrAfter(skewedNotOnOrAfter); verifyConditions(assertion); return assertion; } private void verifyConditions(Assertion assertion) throws EIDASSAMLEngineException { Conditions conditions = assertion.getConditions(); DateTime serverDate = getClock().getCurrentTime(); LOG.debug("serverDate : " + serverDate); if (conditions.getAudienceRestrictions() == null || conditions.getAudienceRestrictions().isEmpty()) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : AudienceRestriction must be present"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "AudienceRestriction must be present"); } if (conditions.getOneTimeUse() == null) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : OneTimeUse must be present"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "OneTimeUse must be present"); } if (conditions.getNotBefore() == null) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : NotBefore must be present"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "NotBefore must be present"); } if (conditions.getNotBefore().isAfter(serverDate)) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Current time is before NotBefore condition"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Current time is before NotBefore condition"); } if (conditions.getNotOnOrAfter() == null) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : NotOnOrAfter must be present"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "NotOnOrAfter must be present"); } if (conditions.getNotOnOrAfter().isBeforeNow()) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Current time is after NotOnOrAfter condition"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Current time is after NotOnOrAfter condition"); } if (assertion.getConditions().getNotOnOrAfter().isBefore(serverDate)) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Token date expired (getNotOnOrAfter = " + assertion.getConditions().getNotOnOrAfter() + ", server_date: " + serverDate + ")"); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Token date expired (getNotOnOrAfter = " + assertion.getConditions().getNotOnOrAfter() + " ), server_date: " + serverDate); } } private void verifyMethodBearer(String userIP, Assertion assertion) throws EIDASSAMLEngineException { boolean ipValidate = getCoreProperties().isIpValidation(); if (ipValidate) { LOG.trace("Verified method Bearer"); Subject subject = assertion.getSubject(); if (null == subject) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : subject is null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "subject is null."); } List<SubjectConfirmation> subjectConfirmations = subject.getSubjectConfirmations(); if (null == subjectConfirmations || subjectConfirmations.isEmpty()) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : SubjectConfirmations are null or empty."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "SubjectConfirmations are null or empty."); } for (final SubjectConfirmation element : subjectConfirmations) { boolean isBearer = SubjectConfirmation.METHOD_BEARER.equals(element.getMethod()); SubjectConfirmationData subjectConfirmationData = element.getSubjectConfirmationData(); if (null == subjectConfirmationData) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : subjectConfirmationData is null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "subjectConfirmationData is null."); } String address = subjectConfirmationData.getAddress(); if (isBearer) { if (StringUtils.isBlank(userIP)) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : browser_ip is null or empty."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "browser_ip is null or empty."); } else if (StringUtils.isBlank(address)) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : token_ip attribute is null or empty."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "token_ip attribute is null or empty."); } } boolean ipEqual = address.equals(userIP); // Validation ipUser if (!ipEqual) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : SubjectConfirmation BEARER: IPs doesn't match : token_ip [{}] browser_ip [{}]", address, userIP); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "IPs doesn't match : token_ip (" + address + ") browser_ip (" + userIP + ")"); } } } } private void validateAssertionSignatures(Response response) throws EIDASSAMLEngineException { try { boolean validateSign = getCoreProperties().isValidateSignature(); if (validateSign) { X509Certificate signatureCertificate = getExtensionProcessor() .getResponseSignatureCertificate(response.getIssuer().getValue()); for (Assertion a : response.getAssertions()) { if (a.isSigned() && null != a.getSignature()) { getSigner().validateSignature(a, null == signatureCertificate ? null : ImmutableSet.of(signatureCertificate)); } } } } catch (EIDASSAMLEngineException e) { EIDASSAMLEngineException exc = new EIDASSAMLEngineException( EidasErrorKey.INVALID_ASSERTION_SIGNATURE.errorCode(), EidasErrorKey.INVALID_ASSERTION_SIGNATURE.errorMessage(), e); throw exc; } } private AuthnRequest validateSignature(AuthnRequest request) throws EIDASSAMLEngineException { boolean validateSign = getCoreProperties().isValidateSignature(); if (validateSign) { LOG.trace("Validate request Signature."); if (!request.isSigned() || null == request.getSignature()) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "No signature"); } try { X509Certificate signatureCertificate = getExtensionProcessor() .getResponseSignatureCertificate(request.getIssuer().getValue()); return (AuthnRequest) getSigner().validateSignature(request, null == signatureCertificate ? null : ImmutableSet.of(signatureCertificate)); } catch (EIDASSAMLEngineException e) { LOG.error(SAML_EXCHANGE, "BUSINESS EXCEPTION : SAMLEngineException validateSignature: " + e, e.getMessage(), e); throw e; } } return request; } private Response validateSignatureAndAssertionSignatures(Response response) throws EIDASSAMLEngineException { boolean validateSign = getCoreProperties().isValidateSignature(); if (validateSign) { LOG.trace("Validate response Signature."); if (!response.isSigned() || null == response.getSignature()) { throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "No signature"); } String country = getCountry(response.getSignature().getKeyInfo()); LOG.debug(SAML_EXCHANGE, "Response received from country: " + country); try { response = validateSignatureAndDecrypt(response); validateAssertionSignatures(response); } catch (EIDASSAMLEngineException e) { LOG.error(SAML_EXCHANGE, "BUSINESS EXCEPTION : SAMLEngineException validateSignature: " + e, e.getMessage(), e); throw e; } } return response; } /** * Validate SAML. * * @param tokenSaml the token SAML * @return the signable SAML object * @throws EIDASSAMLEngineException the EIDASSAML engine exception */ private AuthnRequest validateRequestBytes(byte[] requestBytes) throws EIDASSAMLEngineException { LOG.trace("Validate request bytes."); if (null == requestBytes) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Saml request bytes are null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Saml request bytes are null."); } LOG.trace("Generate SAML Request."); AuthnRequest request = (AuthnRequest) unmarshall(requestBytes); request = validateSignature(request); validateSchema(request); return request; } private Response validateResponseBytes(byte[] responseBytes) throws EIDASSAMLEngineException { LOG.trace("Validate response bytes."); if (null == responseBytes) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : Saml response bytes are null."); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), "Saml response bytes are null."); } LOG.trace("Generate SAML Response."); Response response = (Response) unmarshall(responseBytes); response = validateSignatureAndAssertionSignatures(response); validateSchema(response); return response; } private static void validateSchema(SignableSAMLObject samlObject) throws EIDASSAMLEngineException { LOG.trace("Validate Schema."); ValidatorSuite validatorSuite = Configuration.getValidatorSuite("saml2-core-schema-validator"); try { validatorSuite.validate(samlObject); } catch (ValidationException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException.", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : ValidationException.", e); throw new EIDASSAMLEngineException(EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorCode(), EidasErrorKey.MESSAGE_VALIDATION_ERROR.errorMessage(), e); } } /** * This is used by the validator * * @// TODO: 13/05/2016 move this to a specific extend of the SAMLengine dedicated to validator */ @Override @Deprecated public IRequestMessage generateEIDASAuthnRequestWithoutValidation(IAuthenticationRequest request) throws EIDASSAMLEngineException { return generateRequestMessage(request, false, true); } /** * This is used by the validator * * @// TODO: 13/05/2016 move this to a specific extend of the SAMLengine dedicated to validator */ @Override @Deprecated public IRequestMessage generateEIDASAuthnRequestWithoutSign(IAuthenticationRequest request) throws EIDASSAMLEngineException { return generateRequestMessage(request, false, false); } /** * Resign authentication request ( for validation purpose). * * @param originalRequest * @param changeProtocol If true will update the protocol of the resigned request with the one within {@code * request} * @param changeDestination If true will update the destination of the resigned request with the one within {@code * request} * @return the resigned request * @throws EIDASSAMLEngineException the EIDASSAML engine exception * @// TODO: 13/05/2016 move this to a specific extend of the SAMLengine dedicated to validator */ @Override public IRequestMessage resignEIDASAuthnRequest(IRequestMessage originalRequest, boolean changeDestination) throws EIDASSAMLEngineException { LOG.trace("Getting the saml token."); AuthnRequest authnRequestAux = null; // Obtaining new saml Token byte[] tokenSaml = originalRequest.getMessageBytes(); IAuthenticationRequest authenticationRequest = originalRequest.getRequest(); authnRequestAux = (AuthnRequest) unmarshall(tokenSaml); authnRequestAux.setProtocolBinding( getExtensionProcessor().getProtocolBinding(authenticationRequest, getCoreProperties())); if (changeDestination) { authnRequestAux.setDestination(authenticationRequest.getDestination()); } // copy constructor && assignment LOG.trace("copy contructor and assigment of token."); try { IRequestMessage resignedAuthnRequest = new BinaryRequestMessage(authenticationRequest, signAndMarshallRequest(authnRequestAux)); return resignedAuthnRequest; } catch (EIDASSAMLEngineException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : resignEIDASAuthnRequest : Sign and Marshall.{}", e.getMessage()); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : resignEIDASAuthnRequest : Sign and Marshall.{}", e); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } } /** * Resign a request (for validation purpose). * * @return the resigned request * @throws EIDASSAMLEngineException the EIDASSAML engine exception * @deprecated this method is only used by the validator */ @Override @Deprecated public byte[] reSignRequest(byte[] requestBytes) throws EIDASSAMLEngineException { LOG.trace("Generate SAMLAuthnRequest."); AuthnRequest authnRequest = null; authnRequest = (AuthnRequest) unmarshall(requestBytes); releaseExtensionsDom(authnRequest); if (null == authnRequest) { throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorCode(), "invalid AuthnRequest"); } try { return signAndMarshallRequest(authnRequest); } catch (EIDASSAMLEngineException e) { LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : resignEIDASTokenSAML : Sign and Marshall.", e); LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : resignEIDASTokenSAML : Sign and Marshall.", e.getMessage()); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } } private void releaseExtensionsDom(AuthnRequest authnRequestAux) { if (authnRequestAux.getExtensions() == null) { return; } authnRequestAux.getExtensions().releaseDOM(); authnRequestAux.getExtensions().releaseChildrenDOM(true); } /** * Resigns the saml token checking previously if it is encrypted * * @param tokenSaml * @return * @throws EIDASSAMLEngineException * @deprecated information missing about whom to encrypt the response for */ @Override @Deprecated public byte[] checkAndResignRequest(@Nonnull byte[] requestBytes) throws EIDASSAMLEngineException { AuthnRequest request = (AuthnRequest) unmarshall(requestBytes); request = validateSignature(request); if (null == request) { throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), "BUSINESS EXCEPTION : invalid SAML Request"); } try { return signAndMarshallRequest(request); } catch (EIDASSAMLEngineException e) { LOG.error(SAML_EXCHANGE, "BUSINESS EXCEPTION : checkAndResignEIDASTokenSAML : Sign and Marshall: " + e, e); throw e; } } /** * Decrypt and validate saml respons * * @param responseBytes * @return * @throws EIDASSAMLEngineException */ @Override public byte[] checkAndDecryptResponse(@Nonnull byte[] responseBytes) throws EIDASSAMLEngineException { Response response = null; response = (Response) unmarshall(responseBytes); response = validateSignatureAndAssertionSignatures(response); if (null == response) { throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), "BUSINESS EXCEPTION : invalid SAML Response"); } try { return marshall(response); } catch (EIDASSAMLEngineException e) { LOG.debug(SAML_EXCHANGE, "BUSINESS EXCEPTION : checkAndResignEIDASTokenSAML : Sign and Marshall.", e); LOG.info(SAML_EXCHANGE, "BUSINESS EXCEPTION : checkAndResignEIDASTokenSAML : Sign and Marshall.", e.getMessage()); throw new EIDASSAMLEngineException(EidasErrorKey.INTERNAL_ERROR.errorCode(), EidasErrorKey.INTERNAL_ERROR.errorMessage(), e); } } /** * Returns true when the input contains an encrypted SAML Response * * @param tokenSaml * @return * @throws EIDASSAMLEngineException */ @Override public boolean isEncryptedSamlResponse(byte[] tokenSaml) throws EIDASSAMLEngineException { SignableSAMLObject samlObject = null; samlObject = (SignableSAMLObject) unmarshall(tokenSaml); if (samlObject instanceof Response) { Response response = (Response) samlObject; return response.getEncryptedAssertions() != null && !response.getEncryptedAssertions().isEmpty(); } return false; } private boolean tryProcessExtensions(ExtensionProcessorI extensionProcessor, AuthnRequest samlRequest) throws ValidationException, EIDASSAMLEngineException { IAuthenticationRequest request = extensionProcessor.processExtensions("BE", samlRequest, null, null); //format discriminator goes here if (request != null) { boolean validRequest = extensionProcessor.isValidRequest(samlRequest); LOG.debug("tryProcessExtensions with " + extensionProcessor.getClass().getName() + " returns: " + validRequest); return validRequest; } return false; } }