/******************************************************************************* * 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.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.NameID; import org.opensaml.saml2.core.NameIDPolicy; import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.Scoping; import org.opensaml.saml2.metadata.SPSSODescriptor; import org.opensaml.xml.XMLObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import at.asitplus.eidas.specific.connector.MSeIDASNodeConstants; import at.asitplus.eidas.specific.connector.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.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.modules.pvp2.api.reqattr.EAAFRequestedAttribute; import at.gv.egiz.eaaf.modules.pvp2.api.reqattr.EAAFRequestedAttributes; import at.gv.egiz.eaaf.modules.pvp2.api.validation.IAuthnRequestValidator; import at.gv.egiz.eaaf.modules.pvp2.exception.NameIDFormatNotSupportedException; import eu.eidas.auth.commons.protocol.eidas.LevelOfAssurance; public class AuthnRequestValidator implements IAuthnRequestValidator { private static final Logger log = LoggerFactory.getLogger(AuthnRequestValidator.class); @Autowired(required=true) private IConfiguration basicConfig; @Override public void validate(HttpServletRequest httpReq, IRequest pendingReq, AuthnRequest authnReq, SPSSODescriptor spSSODescriptor) throws AuthnRequestValidatorException { try { //validate NameIDPolicy NameIDPolicy nameIDPolicy = authnReq.getNameIDPolicy(); if (nameIDPolicy != null) { String nameIDFormat = nameIDPolicy.getFormat(); if (nameIDFormat != null) { if ( !(NameID.TRANSIENT.equals(nameIDFormat) || NameID.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 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 String providerName = authnReq.getProviderName(); if (StringUtils.isEmpty(providerName)) log.info("Authn. request contains NO SP friendlyName"); else pendingReq.setRawDataToTransaction(MSeIDASNodeConstants.DATA_PROVIDERNAME, spEntityId); //post-process requested LoA List reqLoA = extractLoA(authnReq); 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); List allowedLoA = new ArrayList<>(); for (String loa : reqLoA) { try { 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 = intLoa.getValue(); } if (!allowedLoA.contains(selectedLoA)) { log.debug("Allow LoA: {} for Client: {}", selectedLoA, pendingReq.getServiceProviderConfiguration().getUniqueIdentifier()); allowedLoA.add(selectedLoA); } } catch (IllegalArgumentException e) { log.warn("LoA: {} is currently NOT supported and it will be ignored.", loa); } } pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class).setRequiredLoA(allowedLoA); //post-process requested LoA comparison-level String reqLoAComperison = extractComparisonLevel(authnReq); pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class).setLoAMachtingMode(reqLoAComperison); //validate and process requested attributes boolean sectorDetected = false; List requestedAttributes = authnReq.getExtensions().getUnknownXMLObjects(); for (XMLObject reqAttrObj : requestedAttributes) { if (reqAttrObj instanceof EAAFRequestedAttributes) { EAAFRequestedAttributes reqAttr = (EAAFRequestedAttributes)reqAttrObj; if (reqAttr.getAttributes() != null && reqAttr.getAttributes().size() != 0 ) { for (EAAFRequestedAttribute el : reqAttr.getAttributes()) { log.trace("Processing req. attribute '" + el.getName() + "' ... "); if (el.getName().equals(PVPAttributeDefinitions.EID_SECTOR_FOR_IDENTIFIER_NAME)) { if (el.getAttributeValues() != null && el.getAttributeValues().size() == 1) { String sectorId = el.getAttributeValues().get(0).getDOM().getTextContent(); ServiceProviderConfiguration spConfig = pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class); try { spConfig.setbPKTargetIdentifier(sectorId); sectorDetected = true; } catch (EAAFException e) { log.info("Requested sector: " + sectorId + " DOES NOT match to allowed sectors for SP: " + spConfig.getUniqueIdentifier()); } } else log.info("Req. attribute '" + el.getName() + "' contains NO or MORE THEN ONE attribute-values. Ignore full req. attribute"); } 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.info("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"}); } } catch (EAAFStorageException e) { log.info("Can NOT store Authn. Req. data into pendingRequest." , e); throw new AuthnRequestValidatorException("internal.02", null, e); } } private String extractComparisonLevel(AuthnRequest authnReq) { if (authnReq.getRequestedAuthnContext() != null) { RequestedAuthnContext authContext = authnReq.getRequestedAuthnContext(); return authContext.getComparison().toString(); } return null; } private List extractLoA(AuthnRequest authnReq) throws AuthnRequestValidatorException { List result = new ArrayList(); if (authnReq.getRequestedAuthnContext() != null) { 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 (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) { 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; } }