/* * 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.modules.auth.eidas.v2.handler; import static at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.EidasResponseUtils.processCountryCode; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.lang.NonNull; import com.google.common.collect.ImmutableSortedSet; import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidPostProcessingException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidPreProcessingException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidasAttributeException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.ConnectorEidasAttributeRegistry; import at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.EidasResponseUtils; import at.asitplus.eidas.specific.modules.core.eidas.EidasConstants; import at.gv.e_government.reference.namespace.persondata._20020228.PostalAddressType; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.data.EaafConstants; import at.gv.egiz.eaaf.core.api.idp.IConfigurationWithSP; import at.gv.egiz.eaaf.core.api.idp.ISpConfiguration; import at.gv.egiz.eaaf.core.impl.utils.KeyValueUtils; import eu.eidas.auth.commons.attribute.AttributeDefinition; import eu.eidas.auth.commons.attribute.ImmutableAttributeMap; import eu.eidas.auth.commons.light.impl.LightRequest.Builder; import eu.eidas.auth.commons.protocol.eidas.SpType; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class AbstractEidProcessor implements INationalEidProcessor { @Autowired protected ConnectorEidasAttributeRegistry attrRegistry; @Autowired protected IConfigurationWithSP basicConfig; @Override public final void preProcess(IRequest pendingReq, Builder authnRequestBuilder, String countryCode) throws EidPreProcessingException { // validate current state validateSelectionWithState(pendingReq, countryCode); // build country-specific authentication request buildNameIdPolicy(authnRequestBuilder, countryCode); buildLevelOfAssurance(pendingReq.getServiceProviderConfiguration(), authnRequestBuilder); buildProviderNameAndRequesterIdAttribute(pendingReq, authnRequestBuilder); buildRequestedAttributes(authnRequestBuilder); } @Override public final SimpleEidasData postProcess(Map eidasAttrMap) throws EidPostProcessingException, EidasAttributeException { SimpleEidasData.SimpleEidasDataBuilder builder = SimpleEidasData.builder() .personalIdentifier(EidasResponseUtils.processPersonalIdentifier( eidasAttrMap.get(EidasConstants.eIDAS_ATTR_PERSONALIDENTIFIER))) // MDS attributes .citizenCountryCode(processCountryCode(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_PERSONALIDENTIFIER))) .pseudonym(processPseudonym(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_PERSONALIDENTIFIER))) .familyName(processFamilyName(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_CURRENTFAMILYNAME))) .givenName(processGivenName(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_CURRENTGIVENNAME))) .dateOfBirth(processDateOfBirth(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_DATEOFBIRTH))) // additional attributes .placeOfBirth(processPlaceOfBirth(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_PLACEOFBIRTH))) .birthName(processBirthName(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_BIRTHNAME))) .address(processAddress(eidasAttrMap.get(EidasConstants.eIDAS_ATTR_CURRENTADDRESS))); if (eidasAttrMap.containsKey(EidasConstants.eIDAS_ATTR_TAXREFERENCE)) { builder.taxNumber(EidasResponseUtils.processTaxReference( eidasAttrMap.get(EidasConstants.eIDAS_ATTR_TAXREFERENCE))); } return builder.build(); } /** * Get a Map of country-specific requested attributes. * * @return */ @NonNull protected abstract Map getCountrySpecificRequestedAttributes(); /** * Post-Process the eIDAS CurrentAddress attribute. * * @param currentAddressObj eIDAS current address information * @return current address or null if no attribute is available * @throws EidPostProcessingException if post-processing fails * @throws EidasAttributeException if eIDAS attribute is of a wrong type */ protected PostalAddressType processAddress(Object currentAddressObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processAddress(currentAddressObj); } /** * Post-Process the eIDAS BirthName attribute. * * @param birthNameObj eIDAS birthname information * @return birthName or null if no attribute is available * @throws EidPostProcessingException if post-processing fails * @throws EidasAttributeException if eIDAS attribute is of a wrong type */ protected String processBirthName(Object birthNameObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processBirthName(birthNameObj); } /** * Post-Process the eIDAS PlaceOfBirth attribute. * * @param placeOfBirthObj eIDAS Place-of-Birth information * @return place of Birth or null if no attribute is available * @throws EidPostProcessingException if post-processing fails * @throws EidasAttributeException if eIDAS attribute is of a wrong type */ protected String processPlaceOfBirth(Object placeOfBirthObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processPlaceOfBirth(placeOfBirthObj); } /** * Post-Process the eIDAS DateOfBirth attribute. * * @param dateOfBirthObj eIDAS date-of-birth attribute information * @return formated user's date-of-birth * @throws EidasAttributeException if NO attribute is available * @throws EidPostProcessingException if post-processing fails */ protected String processDateOfBirth(Object dateOfBirthObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processDateOfBirthToString(dateOfBirthObj); } /** * Post-Process the eIDAS GivenName attribute. * * @param givenNameObj eIDAS givenName attribute information * @return formated user's givenname * @throws EidasAttributeException if NO attribute is available * @throws EidPostProcessingException if post-processing fails */ protected String processGivenName(Object givenNameObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processGivenName(givenNameObj); } /** * Post-Process the eIDAS FamilyName attribute. * * @param familyNameObj eIDAS familyName attribute information * @return formated user's familyname * @throws EidasAttributeException if NO attribute is available * @throws EidPostProcessingException if post-processing fails */ protected String processFamilyName(Object familyNameObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processFamilyName(familyNameObj); } /** * Post-Process the eIDAS pseudonym to ERnB unique identifier. * * @param personalIdObj eIDAS PersonalIdentifierAttribute * @return Unique personal identifier without country-code information * @throws EidasAttributeException if NO attribute is available * @throws EidPostProcessingException if post-processing fails */ protected String processPseudonym(Object personalIdObj) throws EidPostProcessingException, EidasAttributeException { return EidasResponseUtils.processPseudonym(personalIdObj); } /** * Set ProviderName and RequestId into eIDAS AuthnRequest. * * @param pendingReq Current pendingRequest * @param authnRequestBuilder AuthnRequest builder */ protected void buildProviderNameAndRequesterIdAttribute(IRequest pendingReq, Builder authnRequestBuilder) { final ISpConfiguration spConfig = pendingReq.getServiceProviderConfiguration(); if (isPublicServiceProvider(pendingReq)) { log.debug("Map {} to 'PublicSector'", spConfig.getAreaSpecificTargetIdentifier()); authnRequestBuilder.spType(SpType.PUBLIC.getValue()); final String providerName = pendingReq.getRawData(Constants.DATA_PROVIDERNAME, String.class); if (basicConfig.getBasicConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_NODE_WORKAROUND_ADD_ALWAYS_PROVIDERNAME, false)) { //TODO: only for eIDAS ref. node 2.0 and 2.1 because it need 'Providername' for if (StringUtils.isNotEmpty(providerName)) { log.debug("Set 'providername' to: {}", providerName); authnRequestBuilder.providerName(providerName); } else { authnRequestBuilder.providerName(basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_NODE_STATIC_PROVIDERNAME_FOR_PUBLIC_SP, Constants.DEFAULT_PROPS_EIDAS_NODE_STATIC_PROVIDERNAME_FOR_PUBLIC_SP)); } } } else { log.debug("Map {} to 'PrivateSector'", spConfig.getAreaSpecificTargetIdentifier()); authnRequestBuilder.spType(SpType.PRIVATE.getValue()); // TODO: switch to RequesterId in further version // set provider name for private sector applications final String providerName = pendingReq.getRawData(Constants.DATA_PROVIDERNAME, String.class); if (StringUtils.isNotEmpty(providerName)) { authnRequestBuilder.providerName(providerName); } authnRequestBuilder.requesterId( generateRequesterId(pendingReq.getRawData(Constants.DATA_REQUESTERID, String.class))); } } /** * Build LoA based on Service-Provider configuration. * * @param spConfig Current SP configuration * @param authnRequestBuilder AuthnRequest builder */ protected void buildLevelOfAssurance(ISpConfiguration spConfig, Builder authnRequestBuilder) { // TODO: set matching mode if eIDAS ref. impl. support this method // TODO: update if eIDAS ref. impl. supports exact matching for non-notified LoA // schemes String loa = EaafConstants.EIDAS_LOA_HIGH; if (spConfig.getRequiredLoA() != null) { if (spConfig.getRequiredLoA().isEmpty()) { log.info("No eIDAS LoA requested. Use LoA HIGH as default"); } else { if (spConfig.getRequiredLoA().size() > 1) { log.info( "Currently only ONE requested LoA is supported for service provider. Use first one ... "); } loa = spConfig.getRequiredLoA().get(0); } } log.debug("Request eIdAS node with LoA: " + loa); authnRequestBuilder.levelsOfAssuranceValues(Arrays.asList(loa)); } private String generateRequesterId(String requesterId) { if (requesterId != null && basicConfig.getBasicConfigurationBoolean( Constants.CONIG_PROPS_EIDAS_NODE_REQUESTERID_USE_HASHED_VERSION, true)) { try { log.trace("Building hashed 'requesterId' for private SP ... "); MessageDigest digest = MessageDigest.getInstance("SHA-256"); String encodedRequesterId = Base64.getEncoder().encodeToString( digest.digest(requesterId.getBytes(StandardCharsets.UTF_8))); log.debug("Set 'requesterId' for: {} to: {}", requesterId, encodedRequesterId); return encodedRequesterId; } catch (NoSuchAlgorithmException e) { log.error("Can NOT generate hashed 'requesterId' from: {}. Use it as it is", requesterId, e); } } return requesterId; } private void buildNameIdPolicy(Builder authnRequestBuilder, String countryCode) { String ccSpecificPolicy = basicConfig.getBasicConfiguration( Constants.CONFIG_PROP_EIDAS_NODE_NAMEIDFORMAT + "." + countryCode.toLowerCase()); if (StringUtils.isNotEmpty(ccSpecificPolicy)) { log.debug("Using specific nameIdFormat:{} to request: {}", ccSpecificPolicy, countryCode); authnRequestBuilder.nameIdFormat(ccSpecificPolicy); } else { log.trace("Using default nameIdFormat to request: {}", countryCode); authnRequestBuilder.nameIdFormat( basicConfig.getBasicConfiguration(Constants.CONFIG_PROP_EIDAS_NODE_NAMEIDFORMAT)); } } private void buildRequestedAttributes(Builder authnRequestBuilder) { // build and add requested attribute set final Map ccSpecificReqAttr = getCountrySpecificRequestedAttributes(); log.debug("Get #{} country-specific requested attributes", ccSpecificReqAttr.size()); final Map mdsReqAttr = attrRegistry.getDefaultAttributeSetFromConfiguration(); log.trace("Get #{} default requested attributes", mdsReqAttr.size()); // put it together ccSpecificReqAttr.putAll(mdsReqAttr); // convert it to eIDAS attributes final ImmutableAttributeMap reqAttrMap = translateToEidasAttributes(ccSpecificReqAttr); authnRequestBuilder.requestedAttributes(reqAttrMap); } private ImmutableAttributeMap translateToEidasAttributes(final Map requiredAttributes) { final ImmutableAttributeMap.Builder builder = ImmutableAttributeMap.builder(); for (final Map.Entry attribute : requiredAttributes.entrySet()) { final String name = attribute.getKey(); final ImmutableSortedSet> byFriendlyName = attrRegistry.getCoreRegistry() .getCoreAttributeRegistry().getByFriendlyName(name); if (!byFriendlyName.isEmpty()) { final AttributeDefinition attributeDefinition = byFriendlyName.first(); builder.put(AttributeDefinition.builder(attributeDefinition).required(attribute.getValue()).build()); } else { log.warn("Can NOT request UNKNOWN attribute: " + attribute.getKey() + " Ignore it!"); } } return builder.build(); } private void validateSelectionWithState(IRequest pendingReq, String countryCode) throws EidPreProcessingException { boolean psNotSupportPrivate = KeyValueUtils.getListOfCsvValues( basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_NOT_SUPPORT_PRIVATE_SP)) .stream() .filter(el -> el.equalsIgnoreCase(countryCode)) .findFirst() .isPresent(); if (!isPublicServiceProvider(pendingReq) && psNotSupportPrivate) { log.warn("Selected country: {} does not support private service providers.", countryCode); throw new EidPreProcessingException("module.eidasauth.07", null); } } private boolean isPublicServiceProvider(IRequest pendingReq) { final ISpConfiguration spConfig = pendingReq.getServiceProviderConfiguration(); final String publicSectorTargetSelector = basicConfig.getBasicConfiguration( Constants.CONIG_PROPS_EIDAS_NODE_PUBLICSECTOR_TARGETS, Constants.POLICY_DEFAULT_ALLOWED_TARGETS); final Pattern p = Pattern.compile(publicSectorTargetSelector); final Matcher m = p.matcher(spConfig.getAreaSpecificTargetIdentifier()); return m.matches(); } }