/******************************************************************************* * 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.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.egovernment.moa.id.advancedlogging.MOAIDEventConstants; import at.gv.egovernment.moa.id.auth.frontend.velocity.VelocityProvider; 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.IOAAuthParameters; import at.gv.egovernment.moa.id.commons.api.IRequest; import at.gv.egovernment.moa.id.commons.api.exceptions.MOAIDException; import at.gv.egovernment.moa.id.moduls.RequestImpl; import at.gv.egovernment.moa.id.protocols.AbstractAuthProtocolModulController; 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.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.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 { public static final String NAME = EIDASProtocol.class.getName(); public static final String PATH = "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; } public String getPath() { return PATH; } //eIDAS metadata end-point @RequestMapping(value = "/eidas/metadata", method = {RequestMethod.GET}) public void eIDASMetadataRequest(HttpServletRequest req, HttpServletResponse resp) throws MOAIDException { //create pendingRequest object EIDASData pendingReq = applicationContext.getBean(EIDASData.class); pendingReq.initialize(req); 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 MOAIDException, IOException { //create pending-request object EIDASData pendingReq = applicationContext.getBean(EIDASData.class); pendingReq.initialize(req); 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); //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.getBasicMOAIDConfiguration(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 IOAAuthParameters oaConfig = authConfig.getOnlineApplicationParameter(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.getIdentityLinkDomainIdentifier(); //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.error("Configuration for eIDAS-node:" + samlReq.getIssuer() + " Destination Country from request (" + reqCC + ") does not match to configuration:" + eIDASTarget); 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"); } //************************************************* //***** 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.setGenericDataToSession(RequestImpl.eIDAS_GENERIC_REQ_DATA_LEVELOFASSURENCE, eIDASSamlReq.getEidasLevelOfAssurance().stringValue()); // - memorize requested attributes pendingReq.setEidasRequestedAttributes(eIDASSamlReq.getRequestedAttributes()); // - memorize whole request pendingReq.setEidasRequest(eIDASSamlReq); // - memorize OA url pendingReq.setOAURL(samlReq.getIssuer()); // - memorize OA config pendingReq.setOnlineApplicationConfiguration(oaConfig); // - memorize service-provider type from eIDAS request String spType = null; if (eIDASSamlReq.getSpType() != null) spType = eIDASSamlReq.getSpType().getValue(); if (MiscUtil.isEmpty(spType)) spType = MetadataUtil.getSPTypeFromMetadata(eIDASNodeEntityDesc); if (MiscUtil.isNotEmpty(spType)) Logger.debug("eIDAS request has SPType:" + spType); else Logger.info("eIDAS request and eIDAS metadata contains NO 'SPType' element."); } catch (MOAIDException e) { Logger.info("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage()); throw e; } catch (EIDASSAMLEngineException e) { Logger.info("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage()); throw new EIDASAuthnRequestProcessingException("eIDAS.06", new Object[]{e.getMessage()}, e); } catch(Exception e) { Logger.warn("eIDAS AuthnRequest preProcessing FAILED. Msg:" + e.getMessage(), e); 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; } }