/* * 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.NotifiedLevelOfAssurance; 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)); NotifiedLevelOfAssurance minimumLoAFromConfig = NotifiedLevelOfAssurance.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 = NotifiedLevelOfAssurance.HIGH; } log.trace("Validate requested LoA to connector configuration minimum LoA: {} ...", minimumLoAFromConfig); final List allowedLoA = new ArrayList<>(); for (final String loa : reqLoA) { try { final NotifiedLevelOfAssurance intLoa = NotifiedLevelOfAssurance.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; } }