From 0f0dcfc7a01c4b3a8b15b12b5257f08797fd0926 Mon Sep 17 00:00:00 2001 From: Thomas <> Date: Fri, 3 Jun 2022 16:04:40 +0200 Subject: refactor(connector): move MS-Connector from new directory 'connector' to 'ms_specific_connector' --- .../verification/AuthnRequestValidator.java | 382 +++++++++++++++++++++ 1 file changed, 382 insertions(+) create mode 100644 ms_specific_connector/src/main/java/at/asitplus/eidas/specific/connector/verification/AuthnRequestValidator.java (limited to 'ms_specific_connector/src/main/java/at/asitplus/eidas/specific/connector/verification') diff --git a/ms_specific_connector/src/main/java/at/asitplus/eidas/specific/connector/verification/AuthnRequestValidator.java b/ms_specific_connector/src/main/java/at/asitplus/eidas/specific/connector/verification/AuthnRequestValidator.java new file mode 100644 index 00000000..23702264 --- /dev/null +++ b/ms_specific_connector/src/main/java/at/asitplus/eidas/specific/connector/verification/AuthnRequestValidator.java @@ -0,0 +1,382 @@ +/* + * Copyright 2018 A-SIT Plus GmbH + * AT-specific eIDAS Connector has been developed in a cooperation between EGIZ, + * A-SIT Plus GmbH, A-SIT, and Graz University of Technology. + * + * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by + * the European Commission - subsequent versions of the EUPL (the "License"); + * You may not use this work except in compliance with the License. + * You may obtain a copy of the License at: + * https://joinup.ec.europa.eu/news/understanding-eupl-v12 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * 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 at.asitplus.eidas.specific.connector.verification; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang3.StringUtils; +import org.opensaml.core.xml.XMLObject; +import org.opensaml.saml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml.saml2.core.AuthnContextComparisonTypeEnumeration; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.core.NameIDPolicy; +import org.opensaml.saml.saml2.core.NameIDType; +import org.opensaml.saml.saml2.core.RequestedAuthnContext; +import org.opensaml.saml.saml2.core.Scoping; +import org.opensaml.saml.saml2.metadata.SPSSODescriptor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import at.asitplus.eidas.specific.core.MsEidasNodeConstants; +import at.asitplus.eidas.specific.core.config.ServiceProviderConfiguration; +import at.gv.egiz.eaaf.core.api.IRequest; +import at.gv.egiz.eaaf.core.api.data.EaafConstants; +import at.gv.egiz.eaaf.core.api.data.ExtendedPvpAttributeDefinitions; +import at.gv.egiz.eaaf.core.api.data.PvpAttributeDefinitions; +import at.gv.egiz.eaaf.core.api.idp.IConfiguration; +import at.gv.egiz.eaaf.core.exceptions.AuthnRequestValidatorException; +import at.gv.egiz.eaaf.core.exceptions.EaafException; +import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; +import at.gv.egiz.eaaf.core.impl.idp.controller.protocols.RequestImpl; +import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils; +import at.gv.egiz.eaaf.modules.pvp2.api.reqattr.EaafRequestedAttribute; +import at.gv.egiz.eaaf.modules.pvp2.api.reqattr.EaafRequestedAttributes; +import at.gv.egiz.eaaf.modules.pvp2.api.validation.IAuthnRequestPostProcessor; +import at.gv.egiz.eaaf.modules.pvp2.exception.NameIdFormatNotSupportedException; +import eu.eidas.auth.commons.protocol.eidas.LevelOfAssurance; + +public class AuthnRequestValidator implements IAuthnRequestPostProcessor { + + private static final Logger log = LoggerFactory.getLogger(AuthnRequestValidator.class); + + @Autowired(required = true) + private IConfiguration basicConfig; + + @Override + public void process(HttpServletRequest httpReq, IRequest pendingReq, AuthnRequest authnReq, + SPSSODescriptor spSsoDescriptor) throws AuthnRequestValidatorException { + try { + // validate NameIDPolicy + final NameIDPolicy nameIdPolicy = authnReq.getNameIDPolicy(); + if (nameIdPolicy != null) { + final String nameIdFormat = nameIdPolicy.getFormat(); + if (nameIdFormat != null) { + if (!(NameIDType.TRANSIENT.equals(nameIdFormat) + || NameIDType.PERSISTENT.equals(nameIdFormat))) { + + throw new NameIdFormatNotSupportedException(nameIdFormat); + + } + + } else { + log.trace("Find NameIDPolicy, but NameIDFormat is 'null'"); + } + } else { + log.trace("AuthnRequest includes no 'NameIDPolicy'"); + } + + // post-process RequesterId + final String spEntityId = extractScopeRequsterId(authnReq); + if (StringUtils.isEmpty(spEntityId)) { + log.info("NO service-provider entityID in Authn. request. Stop authn. process ... "); + throw new AuthnRequestValidatorException("pvp2.22", + new Object[] { "NO relaying-party entityID in Authn. request" }, pendingReq); + + } else { + pendingReq.setRawDataToTransaction(MsEidasNodeConstants.DATA_REQUESTERID, spEntityId); + } + + // post-process ProviderName + final String providerName = authnReq.getProviderName(); + if (StringUtils.isEmpty(providerName)) { + log.info("Authn. request contains NO SP friendlyName"); + } else { + pendingReq.setRawDataToTransaction(MsEidasNodeConstants.DATA_PROVIDERNAME, providerName); + } + + // post-process requested LoA + postprocessLoaLevel(pendingReq, authnReq); + + // post-process requested LoA comparison-level + pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class).setLoAMachtingMode( + extractComparisonLevel(authnReq)); + + // extract information from requested attributes + extractFromRequestedAttriutes(pendingReq, authnReq); + + } catch (final EaafStorageException e) { + log.info("Can NOT store Authn. Req. data into pendingRequest.", e); + throw new AuthnRequestValidatorException("internal.02", null, e); + + } + + } + + private void extractFromRequestedAttriutes(IRequest pendingReq, AuthnRequest authnReq) + throws AuthnRequestValidatorException, EaafStorageException { + // validate and process requested attributes + boolean sectorDetected = false; + + final ServiceProviderConfiguration spConfig = pendingReq.getServiceProviderConfiguration( + ServiceProviderConfiguration.class); + + if (authnReq.getExtensions() != null) { + final List requestedAttributes = authnReq.getExtensions().getUnknownXMLObjects(); + for (final XMLObject reqAttrObj : requestedAttributes) { + if (reqAttrObj instanceof EaafRequestedAttributes) { + final EaafRequestedAttributes reqAttr = (EaafRequestedAttributes) reqAttrObj; + if (reqAttr.getAttributes() != null && reqAttr.getAttributes().size() != 0) { + for (final EaafRequestedAttribute el : reqAttr.getAttributes()) { + log.trace("Processing req. attribute '" + el.getName() + "' ... "); + if (el.getName().equals(PvpAttributeDefinitions.EID_SECTOR_FOR_IDENTIFIER_NAME)) { + sectorDetected = extractBpkTargetIdentifier(el, spConfig); + + } else if (el.getName().equals(ExtendedPvpAttributeDefinitions.EID_TRANSACTION_ID_NAME)) { + extractUniqueTransactionId(el, pendingReq); + + } else if (el.getName().equals(MsEidasNodeConstants.EID_BINDING_PUBLIC_KEY_NAME)) { + extractBindingPublicKey(el, pendingReq); + + } else { + log.debug("Ignore req. attribute: " + el.getName()); + + } + } + + } else { + log.debug("No requested Attributes in Authn. Request"); + + } + + } else { + log.info("Ignore unknown requested attribute: " + reqAttrObj.getElementQName().toString()); + + } + } + } + + if (!sectorDetected) { + log.warn("Authn.Req validation FAILED. Reason: Contains NO or NO VALID target-sector information."); + throw new AuthnRequestValidatorException("pvp2.22", new Object[] { + "NO or NO VALID target-sector information" }); + + } + + } + + private void extractBindingPublicKey(EaafRequestedAttribute el, IRequest pendingReq) + throws EaafStorageException { + if (el.getAttributeValues() != null && el.getAttributeValues().size() == 1) { + final String bindingPubKey = el.getAttributeValues().get(0).getDOM().getTextContent(); + pendingReq.setRawDataToTransaction(MsEidasNodeConstants.EID_BINDING_PUBLIC_KEY_NAME, bindingPubKey); + log.info("Find Binding Public-Key. eIDAS authentication will be used to create an ID Austria Binding"); + + } else { + log.warn( + "Req. attribute '{}' contains NO or MORE THEN ONE attribute-values. Ignore full req. attribute", + el.getName()); + + } + } + + /** + * Extract unique transactionId from AuthnRequest. + * + * @param el Requested attribute from AuthnRequest + * @param pendingReq Current pendingRequest object (has to be of type + * {@link RequestImpl}) + * @return true if transactionId extraction was successful, + * otherwise false + */ + private boolean extractUniqueTransactionId(EaafRequestedAttribute el, IRequest pendingReq) { + if (!(pendingReq instanceof RequestImpl)) { + log.warn( + "Can NOT set unique transactionId from AuthnRequest,because 'PendingRequest' is NOT from Type: {}", + RequestImpl.class.getName()); + + } else { + if (el.getAttributeValues() != null && el.getAttributeValues().size() == 1) { + final String transactionId = el.getAttributeValues().get(0).getDOM().getTextContent(); + ((RequestImpl) pendingReq).setUniqueTransactionIdentifier(transactionId); + log.info("Find transactionId: {} from requesting service. Replace old id: {} ", + transactionId, TransactionIdUtils.getTransactionId()); + TransactionIdUtils.setTransactionId(transactionId); + + return true; + + } else { + log.warn( + "Req. attribute '{}' contains NO or MORE THEN ONE attribute-values. Ignore full req. attribute", + el.getName()); + + } + + } + + return false; + } + + /** + * Extract the bPK target from requested attribute. + * + * @param el Requested attribute from AuthnRequest + * @param spConfig Service-Provider configuration for current process + * @return true if bPK target extraction was successful, otherwise + * false + */ + private boolean extractBpkTargetIdentifier(EaafRequestedAttribute el, + ServiceProviderConfiguration spConfig) { + if (el.getAttributeValues() != null && el.getAttributeValues().size() == 1) { + final String sectorId = el.getAttributeValues().get(0).getDOM().getTextContent(); + try { + spConfig.setBpkTargetIdentifier(sectorId); + return true; + + } catch (final EaafException e) { + log.warn("Requested sector: " + sectorId + " DOES NOT match to allowed sectors for SP: " + + spConfig.getUniqueIdentifier()); + } + + } else { + log.warn("Req. attribute '" + el.getName() + + "' contains NO or MORE THEN ONE attribute-values. Ignore full req. attribute"); + } + + return false; + + } + + private void postprocessLoaLevel(IRequest pendingReq, AuthnRequest authnReq) + throws AuthnRequestValidatorException { + final List reqLoA = extractLoA(authnReq); + log.trace("SP requests LoA with: {}", String.join(", ", reqLoA)); + + LevelOfAssurance minimumLoAFromConfig = LevelOfAssurance.fromString(basicConfig.getBasicConfiguration( + MsEidasNodeConstants.PROP_EIDAS_REQUEST_LOA_MINIMUM_LEVEL, + EaafConstants.EIDAS_LOA_HIGH)); + if (minimumLoAFromConfig == null) { + log.warn("Can not load minimum LoA from configuration. Use LoA: {} as default", + EaafConstants.EIDAS_LOA_HIGH); + minimumLoAFromConfig = LevelOfAssurance.HIGH; + + } + + log.trace("Validate requested LoA to connector configuration minimum LoA: {} ...", + minimumLoAFromConfig); + final List allowedLoA = new ArrayList<>(); + for (final String loa : reqLoA) { + try { + final LevelOfAssurance intLoa = LevelOfAssurance.fromString(loa); + String selectedLoA = EaafConstants.EIDAS_LOA_HIGH; + if (intLoa != null + && intLoa.numericValue() <= minimumLoAFromConfig.numericValue()) { + log.info("Client: {} requested LoA: {} will be upgraded to: {}", + pendingReq.getServiceProviderConfiguration().getUniqueIdentifier(), + loa, + minimumLoAFromConfig); + selectedLoA = minimumLoAFromConfig.getValue(); + + } + + if (!allowedLoA.contains(selectedLoA)) { + log.debug("Allow LoA: {} for Client: {}", + selectedLoA, + pendingReq.getServiceProviderConfiguration().getUniqueIdentifier()); + allowedLoA.add(selectedLoA); + + } + + } catch (final IllegalArgumentException e) { + log.warn("LoA: {} is currently NOT supported and it will be ignored.", loa); + + } + + } + + pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class).setRequiredLoA( + allowedLoA); + + } + + private String extractComparisonLevel(AuthnRequest authnReq) { + if (authnReq.getRequestedAuthnContext() != null) { + final RequestedAuthnContext authContext = authnReq.getRequestedAuthnContext(); + return authContext.getComparison().toString(); + + } + + return null; + } + + private List extractLoA(AuthnRequest authnReq) throws AuthnRequestValidatorException { + final List result = new ArrayList<>(); + if (authnReq.getRequestedAuthnContext() != null) { + final RequestedAuthnContext authContext = authnReq.getRequestedAuthnContext(); + if (authContext.getComparison().equals(AuthnContextComparisonTypeEnumeration.MINIMUM)) { + if (authContext.getAuthnContextClassRefs().isEmpty()) { + log.debug("Authn. Req. contains no requested LoA"); + + } else if (authContext.getAuthnContextClassRefs().size() > 1) { + log.info("Authn. Req. contains MORE THAN ONE requested LoA, but " + + AuthnContextComparisonTypeEnumeration.MINIMUM + " allows only one"); + throw new AuthnRequestValidatorException("pvp2.22", + new Object[] { "Authn. Req. contains MORE THAN ONE requested LoA, but " + + AuthnContextComparisonTypeEnumeration.MINIMUM + " allows only one" }); + + } else { + result.add(authContext.getAuthnContextClassRefs().get(0).getAuthnContextClassRef()); + } + + } else if (authContext.getComparison().equals(AuthnContextComparisonTypeEnumeration.EXACT)) { + for (final AuthnContextClassRef el : authContext.getAuthnContextClassRefs()) { + result.add(el.getAuthnContextClassRef()); + } + + } else { + log.info("Currently only '" + AuthnContextComparisonTypeEnumeration.MINIMUM + "' and '" + + AuthnContextComparisonTypeEnumeration.EXACT + "' are supported"); + throw new AuthnRequestValidatorException("pvp2.22", + new Object[] { "Currently only '" + AuthnContextComparisonTypeEnumeration.MINIMUM + "' and '" + + AuthnContextComparisonTypeEnumeration.EXACT + "' are supported" }); + + } + + } + + return result; + } + + private String extractScopeRequsterId(AuthnRequest authnReq) { + if (authnReq.getScoping() != null) { + final Scoping scoping = authnReq.getScoping(); + if (scoping.getRequesterIDs() != null + && scoping.getRequesterIDs().size() > 0) { + if (scoping.getRequesterIDs().size() == 1) { + return scoping.getRequesterIDs().get(0).getRequesterID(); + } else { + log.info("Authn. request contains more than on RequesterIDs! Only use first one"); + return scoping.getRequesterIDs().get(0).getRequesterID(); + + } + } + } + + return null; + } + +} -- cgit v1.2.3