package at.asitplus.eidas.specific.modules.msproxyservice.protocol; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.opensaml.saml.saml2.core.NameIDType; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.ResourceLoader; import org.springframework.web.util.UriComponentsBuilder; import at.asitplus.eidas.specific.core.gui.StaticGuiBuilderConfiguration; import at.asitplus.eidas.specific.modules.core.eidas.EidasConstants; import at.asitplus.eidas.specific.modules.msproxyservice.MsProxyServiceConstants; import at.asitplus.eidas.specific.modules.msproxyservice.exception.EidasProxyServiceException; import at.asitplus.eidas.specific.modules.msproxyservice.handler.IEidasAttributeHandler; import at.asitplus.eidas.specific.modules.msproxyservice.service.ProxyEidasAttributeRegistry; import at.asitplus.eidas.specific.modules.msproxyservice.utils.EidasProxyServiceUtils; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.data.PvpAttributeDefinitions; import at.gv.egiz.eaaf.core.api.gui.ISpringMvcGuiFormBuilder; import at.gv.egiz.eaaf.core.api.idp.IAction; import at.gv.egiz.eaaf.core.api.idp.IAuthData; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.api.idp.IEidAuthData; import at.gv.egiz.eaaf.core.api.idp.slo.SloInformationInterface; import at.gv.egiz.eaaf.core.exceptions.EaafConfigurationException; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.GuiBuildException; import at.gv.egiz.eaaf.core.impl.data.SloInformationImpl; import eu.eidas.auth.commons.EidasParameterKeys; import eu.eidas.auth.commons.attribute.AttributeDefinition; import eu.eidas.auth.commons.attribute.ImmutableAttributeMap; import eu.eidas.auth.commons.light.ILightRequest; import eu.eidas.auth.commons.light.ILightResponse; import eu.eidas.auth.commons.light.impl.LightResponse; import eu.eidas.auth.commons.light.impl.LightResponse.Builder; import eu.eidas.auth.commons.light.impl.ResponseStatus; import eu.eidas.auth.commons.tx.BinaryLightToken; import eu.eidas.specificcommunication.BinaryLightTokenHelper; import eu.eidas.specificcommunication.SpecificCommunicationDefinitionBeanNames; import eu.eidas.specificcommunication.exception.SpecificCommunicationException; import eu.eidas.specificcommunication.protocol.SpecificCommunicationService; import lombok.extern.slf4j.Slf4j; /** * Result action of a successfully performed eIDAS Proxy-Service authentication. * * @author tlenz * */ @Slf4j public class ProxyServiceAuthenticationAction implements IAction { private static final String PROXYSERVICE_AUTH_ACTION_NAME = "MS-specific eIDAS-Proxy action"; @Autowired ApplicationContext context; @Autowired IConfiguration basicConfig; @Autowired ResourceLoader resourceLoader; @Autowired ISpringMvcGuiFormBuilder guiBuilder; @Autowired ProxyEidasAttributeRegistry attrRegistry; @Override public SloInformationInterface processRequest(IRequest pendingReq, HttpServletRequest httpReq, HttpServletResponse httpResp, IAuthData authData) throws EaafException { if (pendingReq instanceof ProxyServicePendingRequest) { try { final ILightRequest eidasReq = ((ProxyServicePendingRequest) pendingReq).getEidasRequest(); // build eIDAS response final Builder lightRespBuilder = LightResponse.builder(); lightRespBuilder.id(UUID.randomUUID().toString()); lightRespBuilder.inResponseToId(eidasReq.getId()); lightRespBuilder.relayState(eidasReq.getRelayState()); lightRespBuilder.status(ResponseStatus.builder() .statusCode(EidasConstants.SUCCESS_URI) .build()); // build eIDAS attribute result ImmutableAttributeMap eidasAttributes = buildAttributesFromAuthData(authData, eidasReq); injectSubjectNameId(lightRespBuilder, eidasAttributes, eidasReq); // TODO: lightRespBuilder.issuer(basicConfig.getBasicConfiguration( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_NODE_ENTITYID)); lightRespBuilder.levelOfAssurance(authData.getEidasQaaLevel()); lightRespBuilder.attributes(eidasAttributes); // set SLO response object of EAAF framework final SloInformationImpl sloInformation = new SloInformationImpl(); sloInformation.setProtocolType(pendingReq.requestedModule()); sloInformation .setSpEntityID(pendingReq.getServiceProviderConfiguration().getUniqueIdentifier()); // forward to eIDAS Proxy-Service LightResponse eidasResp = lightRespBuilder.build(); logProvidedAttributes(eidasResp); forwardToEidasProxy(pendingReq, httpReq, httpResp, eidasResp); return sloInformation; } catch (ServletException | IOException | GuiBuildException e) { throw new EidasProxyServiceException("eidas.proxyservice.06", null, e); } } else { log.error("eIDAS Proxy-Service authentication requires PendingRequest of Type: {}", ProxyServicePendingRequest.class.getName()); throw new EaafException("eidas.proxyservice.99"); } } private void logProvidedAttributes(LightResponse eidasResp) { if (eidasResp.getAttributes() == null || eidasResp.getAttributes().isEmpty()) { log.warn("Transfer no attributes to eIDAS Node. There is something wrong"); } else { List listOfAllAttributeNames = eidasResp.getAttributes().getDefinitions().stream() .map(el -> el.getFriendlyName()) .collect(Collectors.toList()); log.info("Transfer attributes to eIDAS Node: {}", StringUtils.join(listOfAllAttributeNames, ",")); } } @Override public boolean needAuthentication(IRequest req, HttpServletRequest httpReq, HttpServletResponse httpResp) { return true; } @Override public String getDefaultActionName() { return PROXYSERVICE_AUTH_ACTION_NAME; } /** * Forward eIDAS Light response to eIDAS node. * * @param pendingReq Current pending request. * @param httpReq Current HTTP request * @param httpResp Current HTTP response * @param lightResponse eIDAS LightResponse * @throws EaafConfigurationException In case of a configuration error * @throws IOException In case of a general error * @throws GuiBuildException In case of a GUI rendering error, if http * POST binding is used * @throws ServletException In case of a general error */ public void forwardToEidasProxy(IRequest pendingReq, HttpServletRequest httpReq, HttpServletResponse httpResp, LightResponse lightResponse) throws EaafConfigurationException, IOException, GuiBuildException, ServletException { // put request into shared cache final BinaryLightToken token = putResponseInCommunicationCache(lightResponse); final String tokenBase64 = BinaryLightTokenHelper.encodeBinaryLightTokenBase64(token); // select forward URL regarding the selected environment final String forwardUrl = basicConfig.getBasicConfiguration( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_NODE_FORWARD_URL); if (StringUtils.isEmpty(forwardUrl)) { log.warn("NO ForwardURL defined in configuration. Can NOT forward to eIDAS node! Process stops"); throw new EaafConfigurationException("config.08", new Object[] { MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_NODE_FORWARD_URL }); } log.debug("ForwardURL: " + forwardUrl + " selected to forward eIDAS request"); if (basicConfig.getBasicConfiguration( EidasConstants.CONIG_PROPS_EIDAS_NODE_FORWARD_METHOD, EidasConstants.FORWARD_METHOD_GET).equals(EidasConstants.FORWARD_METHOD_GET)) { log.debug("Use http-redirect for eIDAS node forwarding ... "); // send redirect final UriComponentsBuilder redirectUrl = UriComponentsBuilder.fromHttpUrl(forwardUrl); redirectUrl.queryParam(EidasParameterKeys.TOKEN.toString(), tokenBase64); httpResp.sendRedirect(redirectUrl.build().encode().toString()); } else { log.debug("Use http-post for eIDAS node forwarding ... "); final StaticGuiBuilderConfiguration config = new StaticGuiBuilderConfiguration( basicConfig, pendingReq, EidasConstants.TEMPLATE_POST_FORWARD_NAME, null, resourceLoader); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_ENDPOINT, forwardUrl); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_TOKEN_NAME, EidasParameterKeys.TOKEN.toString()); config.putCustomParameter(null, EidasConstants.TEMPLATE_POST_FORWARD_TOKEN_VALUE, tokenBase64); guiBuilder.build(httpReq, httpResp, config, "Forward to eIDASNode form"); } } @PostConstruct private void checkConfiguration() { // TODO: validate configuration on start-up } private ImmutableAttributeMap buildAttributesFromAuthData(IAuthData authData, ILightRequest eidasReq) { // eIDAS Out-Going and attribute-specific post-processing of authentication data final IEidAuthData eidAuthData = performAuthdataPostprocessing(authData, eidasReq); final ImmutableAttributeMap.Builder attributeMap = ImmutableAttributeMap.builder(); // inject all requested attributres injectRequestedAttributes(attributeMap, eidasReq, eidAuthData); if (eidAuthData.isUseMandate()) { log.debug("Building eIDAS Proxy-Service response with mandate ... "); injectMdsRepesentativeInformation(attributeMap, eidAuthData, eidasReq.getRequestedAttributes()); // work-around that injects nat. person subject to bypass validation on eIDAS // Node injectJurPersonWorkaroundIfRequired(attributeMap, eidasReq, authData); } return attributeMap.build(); } private void injectRequestedAttributes(ImmutableAttributeMap.Builder attributeMap, ILightRequest eidasReq, IEidAuthData eidAuthData) { eidasReq.getRequestedAttributes().getAttributeMap().keySet().stream() .forEach(el -> injectEidasAttribute(attributeMap, eidAuthData, el.getNameUri().toString(), el.isRequired())); } private void injectMdsRepesentativeInformation( ImmutableAttributeMap.Builder attributeMap, IEidAuthData eidAuthData, ImmutableAttributeMap requestedAttributes) { attrRegistry.getRepresentativeAttributesToAddByDefault() .filter(el -> requestedAttributes.getAttributeValuesByNameUri(el) == null) .forEach(el -> injectEidasAttribute(attributeMap, eidAuthData, el, true)); } private void injectEidasAttribute(ImmutableAttributeMap.Builder attributeMap, IEidAuthData eidAuthData, String eidasAttrName, boolean isRequired) { final Optional releatedIdaAttribute = attrRegistry.mapEidasAttributeToSpecificIdaAttribute(eidasAttrName, eidAuthData.isUseMandate()); if (releatedIdaAttribute.isPresent()) { log.trace("Mapping IDA attribute: {} to eIDAS attribute: {}", releatedIdaAttribute.get(), eidasAttrName); final String idaAttrValue = eidAuthData.getGenericData(releatedIdaAttribute.get(), String.class); if (StringUtils.isNotEmpty(idaAttrValue)) { log.debug("Build eIDAS attribute: {} from IDA attribute: {}", eidasAttrName, releatedIdaAttribute .get()); attributeMap.put( attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByName(eidasAttrName), idaAttrValue); } else if (isRequired) { log.warn("eIDAS attribute: {} is marked as required, but no attribute-value available", eidasAttrName); } else { log.info("No IDA attribute: {}, eIDAS attribute: {} will be ignored", releatedIdaAttribute.get(), eidasAttrName); } } else { Optional advancedAttributeHandler = attrRegistry.mapEidasAttributeToAttributeHandler(eidasAttrName); if (advancedAttributeHandler.isPresent()) { final String idaAttrValue = context.getBean(advancedAttributeHandler.get(), IEidasAttributeHandler.class) .buildAttributeValue(eidAuthData); if (StringUtils.isNotEmpty(idaAttrValue)) { log.debug("Build eIDAS attribute: {} by advanced attribute-handler: {}", eidasAttrName, advancedAttributeHandler.get()); attributeMap.put( attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByName(eidasAttrName), idaAttrValue); } else { log.info("Empty attribte-value returned by advanced attribute-handler, eIDAS attribute: {} will be ignored", eidasAttrName); } } else { log.warn("Can not build eIDAS attribute: {}, because there is not corresponding IDA attribute defined", eidasAttrName); } } } private BinaryLightToken putResponseInCommunicationCache(ILightResponse lightResponse) throws ServletException { final BinaryLightToken binaryLightToken; try { final SpecificCommunicationService springManagedSpecificConnectorCommunicationService = (SpecificCommunicationService) context.getBean( SpecificCommunicationDefinitionBeanNames.SPECIFIC_PROXYSERVICE_COMMUNICATION_SERVICE .toString()); binaryLightToken = springManagedSpecificConnectorCommunicationService.putResponse(lightResponse); } catch (final SpecificCommunicationException e) { log.error("Unable to process specific request"); throw new ServletException(e); } return binaryLightToken; } /** * Work-around to inject representative information as nat. person subject to * bypass eIDAS Node validation. * *

* Injection will only be done if this work-around is enabled by * configuration, the mandator is a legal person, and both legal and natural * person subject's is requested. *

* * @param attributeMap Attribute set for eIDAS response * @param eidasReq Incoming eIDAS request * @param authData Authentication data */ private void injectJurPersonWorkaroundIfRequired( ImmutableAttributeMap.Builder attributeMap, ILightRequest eidasReq, IAuthData authData) { //TODO: maybe we have update that check, because we need an activation on Connector level if (isLegalPersonWorkaroundActive() && isLegalPersonMandateAvailable(authData) && EidasProxyServiceUtils.isNaturalPersonRequested(eidasReq) && EidasProxyServiceUtils.isLegalPersonRequested(eidasReq)) { log.debug( "Injecting representative information as nat. person subject to bypass eIDAS Node validation"); final AttributeDefinition attrDefPersonalId = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_PERSONALIDENTIFIER).first(); final AttributeDefinition attrDefFamilyName = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_CURRENTFAMILYNAME).first(); final AttributeDefinition attrDefGivenName = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_CURRENTGIVENNAME).first(); final AttributeDefinition attrDefDateOfBirth = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_DATEOFBIRTH).first(); attributeMap.put(attrDefPersonalId, authData.getGenericData(PvpAttributeDefinitions.BPK_NAME, String.class)); attributeMap.put(attrDefFamilyName, authData.getFamilyName()); attributeMap.put(attrDefGivenName, authData.getGivenName()); attributeMap.put(attrDefDateOfBirth, authData.getDateOfBirth()); } } private boolean isLegalPersonWorkaroundActive() { return basicConfig.getBasicConfigurationBoolean( MsProxyServiceConstants.CONIG_PROPS_EIDAS_PROXY_WORKAROUND_MANDATES_LEGAL_PERSON, false); } private boolean isLegalPersonMandateAvailable(IAuthData authData) { return StringUtils.isNoneEmpty(authData.getGenericData( PvpAttributeDefinitions.MANDATE_LEG_PER_SOURCE_PIN_NAME, String.class)); } /** * Post-processing of authentication data based on requested attributes. * * @param authData Authentication data from ID Austria system. * @param eidasRequest AuthnRequest from foreign country * @return AuthnRequest specific modification of authentication data */ private IEidAuthData performAuthdataPostprocessing(IAuthData authData, ILightRequest eidasRequest) { IEidAuthData idaAuthData = (IEidAuthData) authData; // select advanced attribute handler Set requiredHandlers = eidasRequest.getRequestedAttributes().getAttributeMap().keySet().stream() .map(el -> attrRegistry.mapEidasAttributeToAttributeHandler(el.getNameUri().toString()).orElse(null)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toSet()); if (!requiredHandlers.isEmpty()) { log.info("eIDAS requested attributes requires #{} specific attribute-hander. " + "Starting advanced post-processing of authentication data ... ", requiredHandlers.size()); requiredHandlers.forEach(el -> executeAttributeHandler(el, idaAuthData)); } return idaAuthData; } private void executeAttributeHandler(String handlerClass, IEidAuthData authData) { try { IEidasAttributeHandler handler = context.getBean(handlerClass, IEidasAttributeHandler.class); log.trace("Perfom authData post-processing by using: {}", handler.getClass().getName()); handler.performAuthDataPostprocessing(authData); } catch (Exception e) { log.error("No custom attribute-handler implementation for: {}. Operation can NOT be performed", handlerClass, e); } } private void injectSubjectNameId(Builder lightRespBuilder, ImmutableAttributeMap eidasAttributes, ILightRequest eidasReq) { if (NameIDType.PERSISTENT.equals(eidasReq.getNameIdFormat())) { lightRespBuilder.subjectNameIdFormat(NameIDType.PERSISTENT); final AttributeDefinition attrDefPersonalId = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_PERSONALIDENTIFIER).first(); final AttributeDefinition attrDefJurPersonalId = attrRegistry.getCoreRegistry().getCoreAttributeRegistry().getByFriendlyName( EidasConstants.eIDAS_ATTR_LEGALPERSONIDENTIFIER).first(); // set SubjectNameId as same as PersonalIdentifier String subjectNameId = (String) eidasAttributes.getFirstValue(attrDefPersonalId); if (subjectNameId != null) { lightRespBuilder.subject(subjectNameId); } else { lightRespBuilder.subject((String) eidasAttributes.getFirstValue(attrDefJurPersonalId)); } } else { lightRespBuilder.subject(UUID.randomUUID().toString()); lightRespBuilder.subjectNameIdFormat(NameIDType.TRANSIENT); } } }