/******************************************************************************* * 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.demoOA.servlet.pvp2; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.text.MessageFormat; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.StringUtils; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.runtime.RuntimeConstants; import org.joda.time.DateTime; import org.opensaml.common.SAMLObject; import org.opensaml.common.binding.BasicSAMLMessageContext; import org.opensaml.common.impl.SecureRandomIdentifierGenerator; import org.opensaml.common.xml.SAMLConstants; import org.opensaml.saml2.binding.encoding.HTTPPostEncoder; import org.opensaml.saml2.binding.encoding.HTTPRedirectDeflateEncoder; import org.opensaml.saml2.core.AuthnContextClassRef; import org.opensaml.saml2.core.AuthnContextComparisonTypeEnumeration; import org.opensaml.saml2.core.AuthnRequest; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.NameIDPolicy; import org.opensaml.saml2.core.NameIDType; import org.opensaml.saml2.core.RequestedAuthnContext; import org.opensaml.saml2.core.RequesterID; import org.opensaml.saml2.core.Scoping; import org.opensaml.saml2.metadata.EntityDescriptor; import org.opensaml.saml2.metadata.SingleSignOnService; import org.opensaml.saml2.metadata.impl.SingleSignOnServiceBuilder; import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider; import org.opensaml.ws.transport.http.HttpServletResponseAdapter; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallingException; import org.opensaml.xml.io.Unmarshaller; import org.opensaml.xml.io.UnmarshallingException; import org.opensaml.xml.security.x509.KeyStoreX509CredentialAdapter; import org.opensaml.xml.security.x509.X509Credential; import org.opensaml.xml.signature.Signature; import org.opensaml.xml.signature.SignatureConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.SAXException; import at.gv.egiz.eaaf.core.api.data.EAAFConstants; import at.gv.egiz.eaaf.core.impl.utils.DOMUtils; import at.gv.egovernment.moa.id.demoOA.Configuration; import at.gv.egovernment.moa.id.demoOA.exception.ConfigurationException; import at.gv.egovernment.moa.id.demoOA.utils.SAML2Utils; import at.gv.egovernment.moa.util.MiscUtil; /** * Servlet implementation class Authenticate */ public class Authenticate extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory .getLogger(Authenticate.class); /** * @see HttpServlet#HttpServlet() */ public Authenticate() { super(); final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); try { builder = factory.newDocumentBuilder(); } catch (final ParserConfigurationException e) { log.warn("PVP2 AuthenticationServlet can not be initialized.", e); } } DocumentBuilder builder; // generate AuthenticationRequest protected void process(HttpServletRequest request, HttpServletResponse response, Map legacyParameter) throws ServletException, IOException { try { final Configuration config = Configuration.getInstance(); config.initializePVP2Login(); AuthnRequest authReq = SAML2Utils .createSAMLObject(AuthnRequest.class); final SecureRandomIdentifierGenerator gen = new SecureRandomIdentifierGenerator(); authReq.setID(gen.generateIdentifier()); final String relayState = String.valueOf(RandomUtils.nextLong()); if (config.useRedirectBindingResponse()) { authReq.setAssertionConsumerServiceIndex(1); } else { authReq.setAssertionConsumerServiceIndex(0); } authReq.setAttributeConsumingServiceIndex(0); authReq.setIssueInstant(new DateTime()); // Subject subject = SAML2Utils.createSAMLObject(Subject.class); // NameID name = SAML2Utils.createSAMLObject(NameID.class); final Issuer issuer = SAML2Utils.createSAMLObject(Issuer.class); String serviceURL = config.getPublicUrlPreFix(request); if (!serviceURL.endsWith("/")) { serviceURL = serviceURL + "/"; } // name.setValue(serviceURL); issuer.setValue(serviceURL); // subject.setNameID(name); // authReq.setSubject(subject); issuer.setFormat(NameIDType.ENTITY); authReq.setIssuer(issuer); if (config.setNameIdPolicy()) { final NameIDPolicy policy = SAML2Utils.createSAMLObject(NameIDPolicy.class); policy.setAllowCreate(true); policy.setFormat(NameIDType.PERSISTENT); authReq.setNameIDPolicy(policy); } final String entityname = config.getPVP2IDPMetadataEntityName(); if (MiscUtil.isEmpty(entityname)) { log.info("No IDP EntityName configurated"); throw new ConfigurationException("No IDP EntityName configurated"); } // get IDP metadata from metadataprovider final HTTPMetadataProvider idpmetadata = config.getMetaDataProvier(); final EntityDescriptor idpEntity = idpmetadata.getEntityDescriptor(entityname); if (idpEntity == null) { log.info("IDP EntityName is not found in IDP Metadata"); throw new ConfigurationException("IDP EntityName is not found in IDP Metadata"); } // select authentication-service url from metadata SingleSignOnService redirectEndpoint = null; for (final SingleSignOnService sss : idpEntity.getIDPSSODescriptor(SAMLConstants.SAML20P_NS) .getSingleSignOnServices()) { // Get the service address for the binding you wish to use if (sss.getBinding().equals(SAMLConstants.SAML2_POST_BINDING_URI) && !config .useRedirectBindingRequest()) { redirectEndpoint = sss; } // Get the service address for the binding you wish to use if (sss.getBinding().equals(SAMLConstants.SAML2_REDIRECT_BINDING_URI) && config .useRedirectBindingRequest()) { redirectEndpoint = sss; } } if (redirectEndpoint == null) { log.warn("Can not find valid EndPoint for SAML2 response"); throw new ConfigurationException("Can not find valid EndPoint for SAML2 response"); } authReq.setDestination(redirectEndpoint.getLocation()); // authReq.setDestination("http://test.test.test"); if (config.setAuthnContextClassRef()) { final RequestedAuthnContext reqAuthContext = SAML2Utils.createSAMLObject(RequestedAuthnContext.class); final AuthnContextClassRef authnClassRef = SAML2Utils.createSAMLObject(AuthnContextClassRef.class); if (MiscUtil.isNotEmpty(config.getAuthnContextClassRefValue())) { authnClassRef.setAuthnContextClassRef(config.getAuthnContextClassRefValue()); } else { authnClassRef.setAuthnContextClassRef("http://www.stork.gov.eu/1.0/citizenQAALevel/4"); } reqAuthContext.setComparison(AuthnContextComparisonTypeEnumeration.MINIMUM); reqAuthContext.getAuthnContextClassRefs().add(authnClassRef); authReq.setRequestedAuthnContext(reqAuthContext); } if (StringUtils.isNotEmpty(config.getScopeRequesterId())) { final Scoping scope = SAML2Utils.createSAMLObject(Scoping.class); final RequesterID requesterId = SAML2Utils.createSAMLObject(RequesterID.class); requesterId.setRequesterID(config.getScopeRequesterId()); scope.getRequesterIDs().add(requesterId); authReq.setScoping(scope); } if (config.isEidasProxySimulatorEnabled()) { authReq = injectEidasMsProxyAttributes(request, authReq); } // sign authentication request final KeyStore keyStore = config.getPVP2KeyStore(); final X509Credential authcredential = new KeyStoreX509CredentialAdapter( keyStore, config.getPVP2KeystoreAuthRequestKeyAlias(), config.getPVP2KeystoreAuthRequestKeyPassword().toCharArray()); final Signature signer = SAML2Utils.createSAMLObject(Signature.class); signer.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA1); signer.setCanonicalizationAlgorithm(SignatureConstants.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); signer.setSigningCredential(authcredential); authReq.setSignature(signer); if (!config.useRedirectBindingRequest()) { // generate Http-POST Binding message final VelocityEngine engine = new VelocityEngine(); engine.setProperty(RuntimeConstants.ENCODING_DEFAULT, "UTF-8"); engine.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8"); engine.setProperty(RuntimeConstants.ENCODING_DEFAULT, "UTF-8"); engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); engine.setProperty("classpath.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); engine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.SimpleLog4JLogSystem"); engine.init(); final HTTPPostEncoder encoder = new HTTPPostEncoder(engine, "templates/pvp_postbinding_template.html"); final HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter( response, true); final BasicSAMLMessageContext context = new BasicSAMLMessageContext<>(); final SingleSignOnService service = new SingleSignOnServiceBuilder() .buildObject(); service.setBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"); service.setLocation(redirectEndpoint.getLocation()); context.setOutboundSAMLMessageSigningCredential(authcredential); context.setPeerEntityEndpoint(service); context.setOutboundSAMLMessage(authReq); context.setOutboundMessageTransport(responseAdapter); context.setRelayState(relayState); encoder.encode(context); } else { // generate Redirect Binding message final HTTPRedirectDeflateEncoder encoder = new HTTPRedirectDeflateEncoder(); final HttpServletResponseAdapter responseAdapter = new HttpServletResponseAdapter( response, true); final BasicSAMLMessageContext context = new BasicSAMLMessageContext<>(); final SingleSignOnService service = new SingleSignOnServiceBuilder() .buildObject(); service.setBinding(SAMLConstants.SAML2_REDIRECT_BINDING_URI); service.setLocation(redirectEndpoint.getLocation()); context.setOutboundSAMLMessageSigningCredential(authcredential); context.setPeerEntityEndpoint(service); context.setOutboundSAMLMessage(authReq); context.setOutboundMessageTransport(responseAdapter); context.setRelayState(relayState); encoder.encode(context); } } catch (final Exception e) { log.warn("Authentication Request can not be generated", e); throw new ServletException("Authentication Request can not be generated.", e); } } private AuthnRequest injectEidasMsProxyAttributes(HttpServletRequest request, AuthnRequest authReq) throws SAXException, IOException, ParserConfigurationException, MarshallingException, UnmarshallingException { // build extension from template final String xmlTemplate = IOUtils.toString( Authenticate.class.getResourceAsStream("/templates/reqAttributes.xml"), StandardCharsets.UTF_8); final String target = EAAFConstants.URN_PREFIX_EIDAS + "AT+" + getParameterOrDefault(request, "eidasCountry", "DE"); final String loa = EAAFConstants.EIDAS_LOA_PREFIX + getParameterOrDefault(request, "loa", "high"); final String eidasConnector = "https://simple.test/" + getParameterOrDefault(request, "eidasIdPostfix", "test"); final String xmlString = MessageFormat.format(xmlTemplate, target, loa, eidasConnector); log.debug("Formated requested attributes: " + xmlString); final Document extension = DOMUtils.parseDocument(xmlString, false, null, null); // marshalle, inject, and unmarshalle request to set extension // TODO: find better solution, be it is good enough for a first simple test DocumentBuilder builder; final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); builder = factory.newDocumentBuilder(); final Document document = builder.newDocument(); final Marshaller out = org.opensaml.xml.Configuration.getMarshallerFactory().getMarshaller(authReq); out.marshall(authReq, document); final Node extElement = document.importNode(extension.getDocumentElement(), true); // document.getDocumentElement().appendChild(extElement); document.getDocumentElement().insertBefore(extElement, document.getChildNodes().item(2)); final Unmarshaller in = org.opensaml.xml.Configuration.getUnmarshallerFactory().getUnmarshaller(document .getDocumentElement()); return (AuthnRequest) in.unmarshall(document.getDocumentElement()); } private String getParameterOrDefault(HttpServletRequest request, String paramName, String defaultValue) { final String reqParam = request.getParameter(paramName); if (MiscUtil.isEmpty(reqParam)) { return defaultValue; } else { return reqParam; } } /** * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse * response) */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { process(request, response, null); } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { process(request, response, null); } }