/******************************************************************************* * Copyright 2014 Federal Chancellery Austria * MOA-ID has been developed in a cooperation between BRZ, the Federal * Chancellery Austria - ICT staff unit, and Graz University of Technology. * * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "Licence"); * You may not use this work except in compliance with the Licence. * You may obtain a copy of the Licence at: * http://www.osor.eu/eupl/ * * Unless required by applicable law or agreed to in writing, software * distributed under the Licence is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the Licence for the specific language governing permissions and * limitations under the Licence. * * 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.gv.egovernment.moa.id.protocols.eidas; import java.io.IOException; import java.io.StringWriter; import java.net.URI; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.velocity.Template; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.opensaml.saml2.core.StatusCode; import org.opensaml.saml2.metadata.AssertionConsumerService; import org.opensaml.saml2.metadata.EntityDescriptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.idp.IModulInfo; import at.gv.egiz.eaaf.core.api.idp.ISPConfiguration; import at.gv.egiz.eaaf.core.exceptions.EAAFException; import at.gv.egiz.eaaf.core.impl.gui.velocity.VelocityProvider; import at.gv.egiz.eaaf.core.impl.idp.controller.AbstractAuthProtocolModulController; import at.gv.egiz.eaaf.core.impl.utils.KeyValueUtils; import at.gv.egovernment.moa.id.advancedlogging.MOAIDEventConstants; import at.gv.egovernment.moa.id.auth.modules.eidas.Constants; import at.gv.egovernment.moa.id.auth.modules.eidas.engine.MOAeIDASChainingMetadataProvider; import at.gv.egovernment.moa.id.auth.modules.eidas.engine.MOAeIDASMetadataProviderDecorator; import at.gv.egovernment.moa.id.auth.modules.eidas.exceptions.EIDASAuthnRequestProcessingException; import at.gv.egovernment.moa.id.auth.modules.eidas.exceptions.EIDASAuthnRequestValidationException; import at.gv.egovernment.moa.id.auth.modules.eidas.exceptions.EIDASException; import at.gv.egovernment.moa.id.auth.modules.eidas.utils.SAMLEngineUtils; import at.gv.egovernment.moa.id.commons.MOAIDAuthConstants; import at.gv.egovernment.moa.id.commons.MOAIDConstants; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.logging.Logger; import at.gv.egovernment.moa.util.MiscUtil; import eu.eidas.auth.commons.EidasStringUtil; import eu.eidas.auth.commons.attribute.ImmutableAttributeMap; import eu.eidas.auth.commons.protocol.IAuthenticationRequest; import eu.eidas.auth.commons.protocol.IResponseMessage; import eu.eidas.auth.commons.protocol.eidas.IEidasAuthenticationRequest; import eu.eidas.auth.commons.protocol.eidas.impl.EidasAuthenticationRequest; import eu.eidas.auth.commons.protocol.impl.AuthenticationResponse; import eu.eidas.auth.commons.protocol.impl.AuthenticationResponse.Builder; import eu.eidas.auth.commons.protocol.impl.SamlNameIdFormat; import eu.eidas.auth.engine.ProtocolEngineI; import eu.eidas.auth.engine.metadata.MetadataUtil; import eu.eidas.engine.exceptions.EIDASSAMLEngineException; /** * eIDAS Protocol Support for outbound authentication and metadata generation * * @author tlenz */ @Controller public class EIDASProtocol extends AbstractAuthProtocolModulController implements IModulInfo { public static final String eIDAS_GENERIC_REQ_DATA_LEVELOFASSURENCE = "eIDAS_GENERIC_REQ_DATA_LEVELOFASSURENCE"; public static final String NAME = EIDASProtocol.class.getName(); public static final String PATH = "id_eidas"; @Autowired(required=true) MOAeIDASChainingMetadataProvider eIDASMetadataProvider; public EIDASProtocol() { super(); Logger.debug("Registering servlet " + getClass().getName() + " with mappings '" + Constants.eIDAS_HTTP_ENDPOINT_METADATA + "' and '" + Constants.eIDAS_HTTP_ENDPOINT_IDP_COLLEAGUEREQUEST + //"' and '" + Constants.eIDAS_HTTP_ENDPOINT_IDP_POST + "'."); } public String getName() { return NAME; } @Override public String getAuthProtocolIdentifier() { return PATH; } //eIDAS metadata end-point @RequestMapping(value = "/eidas/metadata", method = {RequestMethod.GET}) public void eIDASMetadataRequest(HttpServletRequest req, HttpServletResponse resp) throws EAAFException { //create pendingRequest object EIDASData pendingReq = applicationContext.getBean(EIDASData.class); pendingReq.initialize(req, authConfig); pendingReq.setModule(NAME); pendingReq.setNeedAuthentication(false); pendingReq.setAuthenticated(false); revisionsLogger.logEvent( pendingReq.getUniqueSessionIdentifier(), pendingReq.getUniqueTransactionIdentifier(), MOAIDEventConstants.TRANSACTION_IP, req.getRemoteAddr()); EidasMetaDataRequest metadataAction = applicationContext.getBean(EidasMetaDataRequest.class); metadataAction.processRequest(pendingReq, req, resp, null); revisionsLogger.logEvent( pendingReq.getUniqueSessionIdentifier(), pendingReq.getUniqueTransactionIdentifier(), Constants.eIDAS_REVERSIONSLOG_METADATA); } //PVP2.x IDP POST-Binding end-point @RequestMapping(value = "/eidas/ColleagueRequest", method = {RequestMethod.POST}) public void PVPIDPPostRequest(HttpServletRequest req, HttpServletResponse resp) throws IOException, EAAFException { //create pending-request object EIDASData pendingReq = applicationContext.getBean(EIDASData.class); pendingReq.initialize(req, authConfig); pendingReq.setModule(NAME); revisionsLogger.logEvent(MOAIDEventConstants.SESSION_CREATED, pendingReq.getUniqueSessionIdentifier()); revisionsLogger.logEvent(MOAIDEventConstants.TRANSACTION_CREATED, pendingReq.getUniqueTransactionIdentifier()); revisionsLogger.logEvent( pendingReq.getUniqueSessionIdentifier(), pendingReq.getUniqueTransactionIdentifier(), MOAIDEventConstants.TRANSACTION_IP, req.getRemoteAddr()); //preProcess eIDAS request preProcess(req, resp, pendingReq); revisionsLogger.logEvent(pendingReq, Constants.eIDAS_REVERSIONSLOG_IDP_AUTHREQUEST, pendingReq.getEidasRequest().getId()); //AuthnRequest needs authentication pendingReq.setNeedAuthentication(true); //set protocol action, which should be executed after authentication pendingReq.setAction(eIDASAuthenticationRequest.class.getName()); //switch to session authentication performAuthentication(req, resp, pendingReq); } /* First request step - send it to BKU selection for user authentication. After the user credentials and other info are obtained, in the second step the request will be processed and the user redirected */ private void preProcess(HttpServletRequest request, HttpServletResponse response, EIDASData pendingReq) throws MOAIDException { Logger.info("received an eIDaS request"); //get SAML Response and decode it String base64SamlToken = request.getParameter("SAMLRequest"); if (MiscUtil.isEmpty(base64SamlToken)) { Logger.warn("No eIDAS SAMLRequest found in http request."); throw new MOAIDException("eIDAS.06", new Object[]{"HTTP request includes no eIDAS SAML-Request element."}); } try { //decode SAML2 token byte[] decSamlToken = EidasStringUtil.decodeBytesFromBase64(base64SamlToken); //get eIDAS SAML-engine ProtocolEngineI engine = SAMLEngineUtils.createSAMLEngine(eIDASMetadataProvider); String cititzenCountryCode = authConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_NODE_COUNTRYCODE, MOAIDAuthConstants.COUNTRYCODE_AUSTRIA); //**************************************** //***** validate eIDAS request ********* //**************************************** //validate SAML token IAuthenticationRequest samlReq = engine.unmarshallRequestAndValidate(decSamlToken, cititzenCountryCode ); //validate internal JAVA class type if (!(samlReq instanceof IEidasAuthenticationRequest)) { Logger.error("eIDAS AuthnRequst from node:" + samlReq.getIssuer() + " is NOT from Type:" + IEidasAuthenticationRequest.class.getName()); throw new MOAIDException("eIDAS.06", new Object[]{"eIDAS AuthnRequest maps to an wrong internal Type."}); } IEidasAuthenticationRequest eIDASSamlReq = (IEidasAuthenticationRequest) samlReq; //validate Destination against MOA-ID-Auth configuration String reqDestination = eIDASSamlReq.getDestination(); if (MiscUtil.isEmpty(reqDestination) || !reqDestination.startsWith(pendingReq.getAuthURL())) { Logger.info("eIDAS AuthnRequest contains a not valid 'Destination' attribute"); throw new EIDASAuthnRequestValidationException("stork.01", new Object[]{"eIDAS AuthnRequest contains a not valid 'Destination' attribute"}); } //check eIDAS node configuration ISPConfiguration oaConfig = authConfig.getServiceProviderConfiguration(samlReq.getIssuer()); if (oaConfig == null) throw new EIDASAuthnRequestProcessingException("eIDAS.08", new Object[]{samlReq.getIssuer()}); //validate AssertionConsumerServiceURL against metadata EntityDescriptor eIDASNodeEntityDesc = new MOAeIDASMetadataProviderDecorator(eIDASMetadataProvider) .getEntityDescriptor(eIDASSamlReq.getIssuer(), SAMLEngineUtils.getMetadataSigner()); String reqAssertionConsumerServiceURL = eIDASSamlReq.getAssertionConsumerServiceURL(); if (MiscUtil.isNotEmpty(reqAssertionConsumerServiceURL)) { boolean isValid = false; List allowedAssertionConsumerUrl = MetadataUtil.getSPSSODescriptor(eIDASNodeEntityDesc).getAssertionConsumerServices(); for (AssertionConsumerService el : allowedAssertionConsumerUrl) { if (reqAssertionConsumerServiceURL.equals(el.getLocation())) isValid = true; } if (!isValid) { Logger.info("eIDAS AuthnRequest contains a not valid 'AssertionConsumerServiceURL' attribute"); throw new EIDASAuthnRequestValidationException("eIDAS.12", new Object[]{"eIDAS AuthnRequest contains a not valid 'AssertionConsumerServiceURL' attribute"}); } } else { /*TODO: eIDAS SAMLEngine 1.1.0 does not validate and set AssertionConsumerServiceURL in a correct form * * Actually, this step is required because EidasProtocolProcesser.class only use the AssertionConsumerServiceURL * from AuthnRequest to set the 'Destination' attribute in eIDAS Response. However, the AssertionConsumerServiceURL * could be empty in Request, which break the Response building process. */ String assertionConsumerServiceURL = MetadataUtil.getAssertionConsumerUrlFromMetadata( SAMLEngineUtils.getMetadataFetcher(), SAMLEngineUtils.getMetadataSigner(), eIDASSamlReq); if (MiscUtil.isEmpty(assertionConsumerServiceURL)) { Logger.error("eIDAS metadata for node:" + eIDASSamlReq.getIssuer() + " contains NO 'AssertionConsumerServiceURL' element!"); throw new EIDASSAMLEngineException("eIDAS metadata for node:" + eIDASSamlReq.getIssuer() + " contains NO 'AssertionConsumerServiceURL' element!"); } EidasAuthenticationRequest.Builder test = EidasAuthenticationRequest.builder(eIDASSamlReq); test.assertionConsumerServiceURL(assertionConsumerServiceURL); eIDASSamlReq = test.build(); } //validate request country-code against eIDAS node config String reqCC = samlReq.getOriginCountryCode(); String eIDASTarget = oaConfig.getAreaSpecificTargetIdentifier(); //validate eIDAS target Pattern pattern = Pattern.compile("^" + at.gv.egovernment.moa.util.Constants.URN_PREFIX_EIDAS + "\\+[A-Z,a-z]{2}\\+[A-Z,a-z]{2}$"); Matcher matcher = pattern.matcher(eIDASTarget); if (MiscUtil.isEmpty(eIDASTarget) || !matcher.matches()) { Logger.error("Configuration for eIDAS-node:" + samlReq.getIssuer() + " contains wrong formated eIDAS target:" + eIDASTarget); throw new MOAIDException("config.08", new Object[]{samlReq.getIssuer()}); } else { String[] splittedTarget = eIDASTarget.split("\\+"); if (!splittedTarget[2].equalsIgnoreCase(reqCC)) { Logger.debug("Configuration for eIDAS-node:" + samlReq.getIssuer() + " Destination Country from request (" + reqCC + ") does not match to configuration:" + eIDASTarget + " --> Perform additional organisation check ..."); //check if eIDAS domain for bPK calculation is a valid target if (!iseIDASTargetAValidOrganisation(reqCC, splittedTarget[2])) { throw new MOAIDException("eIDAS.01", new Object[]{"Destination Country from request does not match to configuration"}); } } Logger.debug("CountryCode from request matches eIDAS-node configuration target: " + eIDASTarget); } //validate service-provider type from eIDAS request String spType = null; if (eIDASSamlReq.getSpType() != null) spType = eIDASSamlReq.getSpType(); if (MiscUtil.isEmpty(spType)) spType = MetadataUtil.getSPTypeFromMetadata(eIDASNodeEntityDesc); if (MiscUtil.isNotEmpty(spType)) Logger.debug("eIDAS request has SPType:" + spType); else { Logger.warn("eIDAS request and eIDAS metadata contains NO 'SPType' element."); throw new EIDASAuthnRequestProcessingException("eIDAS.06", new Object[]{"eIDAS request and eIDAS metadata contains NO 'SPType' element."}); } //validate if minimal data-set if it is not fully requested //TODO: must be tested!!!! ImmutableAttributeMap reqAttrList = eIDASSamlReq.getRequestedAttributes(); for (URI el : Constants.NATURALPERSONMINIMUMDATASETLIST) { if(reqAttrList.getAttributeValuesByNameUri(el) == null) { Logger.warn("Minimum data-set attribute: " + el + " is not requested."); throw new EIDASAuthnRequestProcessingException("eIDAS.06", new Object[]{"eIDAS request does not contain all attributes of minimum data-set for natural person"}); } } //************************************************* //***** store eIDAS request information ********* //************************************************* // - memorize remote ip pendingReq.setRemoteAddress(request.getRemoteAddr()); // - memorize relaystate String relayState = request.getParameter("RelayState"); pendingReq.setRemoteRelayState(relayState); //store level of assurance pendingReq.setRawDataToTransaction(eIDAS_GENERIC_REQ_DATA_LEVELOFASSURENCE, eIDASSamlReq.getEidasLevelOfAssurance().stringValue()); //set flag if transiend identifier is requested if (MiscUtil.isNotEmpty(eIDASSamlReq.getNameIdFormat()) && eIDASSamlReq.getNameIdFormat().equals(SamlNameIdFormat.TRANSIENT.getNameIdFormat())) pendingReq.setRawDataToTransaction(EIDASData.REQ_PARAM_eIDAS_AUTHN_TRANSIENT_ID, true); else pendingReq.setRawDataToTransaction(EIDASData.REQ_PARAM_eIDAS_AUTHN_TRANSIENT_ID, false); // - memorize requested attributes pendingReq.setEidasRequestedAttributes(eIDASSamlReq.getRequestedAttributes()); // - memorize whole request pendingReq.setEidasRequest(eIDASSamlReq); // - memorize OA url pendingReq.setSPEntityId(samlReq.getIssuer()); // - memorize OA config pendingReq.setOnlineApplicationConfiguration(oaConfig); } catch (MOAIDException e) { Logger.info("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage()); //write revision log entries if (pendingReq != null) revisionsLogger.logEvent(pendingReq, MOAIDEventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); throw e; } catch (EIDASSAMLEngineException e) { Logger.info("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage()); //write revision log entries if (pendingReq != null) revisionsLogger.logEvent(pendingReq, MOAIDEventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); throw new EIDASAuthnRequestProcessingException("eIDAS.06", new Object[]{e.getMessage()}, e); } catch(Exception e) { Logger.warn("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage(), e); //write revision log entries if (pendingReq != null) revisionsLogger.logEvent(pendingReq, MOAIDEventConstants.TRANSACTION_ERROR, pendingReq.getUniqueTransactionIdentifier()); throw new EIDASAuthnRequestProcessingException("eIDAS.06", new Object[]{e.getMessage()}, e); } } public boolean generateErrorMessage(Throwable e, HttpServletRequest request, HttpServletResponse response, IRequest pendingReq) throws Throwable { if (pendingReq != null && pendingReq instanceof EIDASData) { EIDASData eidasReq = (EIDASData) pendingReq; if (eidasReq.getEidasRequest() == null) { Logger.info("Can not build eIDAS ErrorResponse. No eIDAS AuthnRequest found."); return false; } try { Builder eIDASRespBuilder = new AuthenticationResponse.Builder(); eIDASRespBuilder.issuer(pendingReq.getAuthURL() + Constants.eIDAS_HTTP_ENDPOINT_METADATA); if (e instanceof EIDASException) { eIDASRespBuilder.statusCode(((EIDASException) e).getStatusCodeFirstLevel()); eIDASRespBuilder.subStatusCode(((EIDASException) e).getStatusCodeSecondLevel()); eIDASRespBuilder.statusMessage(e.getMessage()); } else if (e instanceof MOAIDException ) { eIDASRespBuilder.statusCode(StatusCode.RESPONDER_URI); eIDASRespBuilder.subStatusCode(StatusCode.AUTHN_FAILED_URI); eIDASRespBuilder.statusMessage(e.getMessage()); } else { eIDASRespBuilder.statusCode(StatusCode.RESPONDER_URI); eIDASRespBuilder.subStatusCode(StatusCode.AUTHN_FAILED_URI); eIDASRespBuilder.statusMessage(e.getMessage()); } eIDASRespBuilder.id(eu.eidas.auth.engine.xml.opensaml.SAMLEngineUtils.generateNCName()); eIDASRespBuilder.inResponseTo(eidasReq.getEidasRequest().getId()); //build response AuthenticationResponse eIDASResp = eIDASRespBuilder.build(); //get eIDAS SAML-engine ProtocolEngineI engine = SAMLEngineUtils.createSAMLEngine(eIDASMetadataProvider); //build response message IResponseMessage eIDASRespMsg = engine.generateResponseErrorMessage(eidasReq.getEidasRequest(),eIDASResp, eidasReq.getRemoteAddress()); String token = EidasStringUtil.encodeToBase64(eIDASRespMsg.getMessageBytes()); VelocityEngine velocityEngine = VelocityProvider.getClassPathVelocityEngine(); Template template = velocityEngine.getTemplate("/resources/templates/eidas_postbinding_template.vm"); VelocityContext context = new VelocityContext(); context.put("RelayState", eidasReq.getRemoteRelayState()); context.put("SAMLResponse", token); Logger.debug("SAMLResponse original: " + token); Logger.debug("Putting assertion consumer url as action: " + eidasReq.getEidasRequest().getAssertionConsumerServiceURL()); context.put("action", eidasReq.getEidasRequest().getAssertionConsumerServiceURL()); Logger.trace("Starting template merge"); StringWriter writer = new StringWriter(); Logger.trace("Doing template merge"); template.merge(context, writer); Logger.trace("Template merge done"); Logger.trace("Sending html content : " + new String(writer.getBuffer())); byte[] content = writer.getBuffer().toString().getBytes("UTF-8"); response.setContentType(MOAIDConstants.DEFAULT_CONTENT_TYPE_HTML_UTF8); response.setContentLength(content.length); response.getOutputStream().write(content); return true; } catch (Exception e1 ) { Logger.error("Generate eIDAS Error-Response failed.", e); } } return false; } public boolean validate(HttpServletRequest request, HttpServletResponse response, IRequest pending) { return false; } private boolean iseIDASTargetAValidOrganisation(String reqCC, String bPKTargetArea) { if (MiscUtil.isNotEmpty(reqCC)) { List allowedOrganisations = KeyValueUtils.getListOfCSVValues( authConfig.getBasicConfiguration(Constants.CONFIG_PROPS_EIDAS_BPK_TARGET_PREFIX + reqCC.toLowerCase())); if (allowedOrganisations.contains(bPKTargetArea)) { Logger.debug(bPKTargetArea + " is a valid OrganisationIdentifier for request-country: "+ reqCC); return true; } } Logger.info("OrganisationIdentifier: " + bPKTargetArea + " is not allowed for country: " + reqCC); return false; } }