/* * 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.NameIDPolicy; import org.opensaml.saml2.core.NameIDType; 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 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 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); // post-process requested LoA comparison-level final String reqLoAComperison = extractComparisonLevel(authnReq); pendingReq.getServiceProviderConfiguration(ServiceProviderConfiguration.class).setLoAMachtingMode( reqLoAComperison); // validate and process requested attributes boolean sectorDetected = false; 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)) { if (el.getAttributeValues() != null && el.getAttributeValues().size() == 1) { final String sectorId = el.getAttributeValues().get(0).getDOM().getTextContent(); final ServiceProviderConfiguration spConfig = pendingReq.getServiceProviderConfiguration( ServiceProviderConfiguration.class); try { spConfig.setBpkTargetIdentifier(sectorId); sectorDetected = true; } catch (final 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 (final 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) { 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; } }