import groovy.xml.XmlSlurper import groovy.xml.slurpersupport.GPathResult import groovy.xml.slurpersupport.NodeChild import java.util.zip.Inflater import java.util.zip.InflaterInputStream /** * Gets the value of the Referer header. * If the header is missing the fallback is returned * * This method is used when SAML IDP / Dispatch Error Redirect is not set * * @param fallback - value to return if the Referer header is missing * @return value of header or fallback */ def getReferer(String fallback) { return request.getHttpHeader('Referer') ?: fallback } def redirect(String url) { outargs.put('nevis.transfer.type', 'redirect') outargs.put('nevis.transfer.destination', url) } String getNormalisedSamlMessage(String parameter) { if (parameter == null) { return } String text byte[] decoded // if parameter is raw xml then continue otherwise try to parse the base64 encoding if (parameter.startsWith("<")) { text = new String(parameter) } else { decoded = parameter.decodeBase64() text = new String(decoded) } return text } String getNodeText(GPathResult xml, String nodeName) { return xml.depthFirst().find { GPathResult node -> { node.name().endsWith(":${nodeName}") || node.name().equalsIgnoreCase(nodeName) } }?.text()?.trim() } String getAttribute(GPathResult xml, String attributeName) { return xml.depthFirst().find { GPathResult node -> { node.attributes().containsKey(attributeName) } }?.attributes()?.get(attributeName) } String getNodeText(String parameter, String nodeName) { String samlMessage = getNormalisedSamlMessage(parameter) if (samlMessage == null) { return } def parser = new XmlSlurper() def xml = parser.parseText(samlMessage) return getNodeText(xml, nodeName) } String getAttribute(String parameter, String attributeName) { String samlMessage = getNormalisedSamlMessage(parameter) if (samlMessage == null) { return } def parser = new XmlSlurper() def xml = parser.parseText(samlMessage) return getAttribute(xml, attributeName) } String getIssuer(String value) { return getNodeText(value, 'Issuer') } String getAttributeConsumingServiceIndex(String value) { return getAttribute(value, 'AttributeConsumingServiceIndex') } String getProtocolBinding(String value) { return getAttribute(value, 'ProtocolBinding') } def dispatchIssuer(i2s, String issuer, boolean secureMode) { def result = i2s.get(issuer) if (result == null) { LOG.info("No SP found for issuer '$issuer'. Hint: check SAML SP Connector patterns.") } // dispatch different idp if artifact binding is enabled if(parameters.get('epdMode') == 'artifact' && result == 'epd'){ LOG.debug("EPD: Artifact mode") result = result + "_artifact" } else if (result == 'main' && secureMode) { LOG.debug("AGOV: Secure mode requested") result = result + "_secure" } response.setResult(result) session.put('saml.inbound.issuer', issuer) session.put('saml.idp.result', result) // remember decision for sub-sequent requests without a SAML message } def dispatchIssuer(i2s, String issuer) { dispatchIssuer(i2s, issuer, false) } def dispatchMessage(i2s, String message) { def issuer = getIssuer(message) def secureMode = (getAttributeConsumingServiceIndex(message) == '10101') def useArtifact = ('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact' == getProtocolBinding(message)) LOG.info("secureMode requested: ${secureMode}") if (issuer == null) { LOG.info("No issuer found in incoming SAML message. Giving up.") } session.put('saml.inbound.issuer', issuer) session.put('agov.idp.use.artifact', '' + useArtifact) dispatchIssuer(i2s, issuer, secureMode) } if (parameters.get('logoutConfirmation') == 'true' && "stepup" == request.getMethod()) { String url = request.currentResource def path = new URL(url).getPath() if (path.endsWith("/logout")) { // next AuthState will show a logout confirmation GUI response.setResult('confirm') return } } // ensure session exists if (request.getSession(false) == null) { session = request.getSession(true).getData() } // issuer (any case) -> ResultCond name def i2s = new TreeMap(String.CASE_INSENSITIVE_ORDER) i2s.put(parameters.get('atb'), 'main') i2s.put(parameters.get('epd_atb'), 'epd') if (parameters.get('spInitiated') == 'true' && inargs.containsKey('SAMLRequest')) { // SP-initiated authentication LOG.debug("found SAMLRequest parameter for SP-initiated authentication") String message = inargs.get('SAMLRequest') dispatchMessage(i2s, message) return } if (inargs.containsKey('SAMLResponse')) { // response to IDP-initiated SAML Logout LOG.debug("found SAMLResponse parameter") String message = inargs.get('SAMLResponse') dispatchMessage(i2s, message) return } if (parameters.get('spInitiated') == 'true' && inargs.containsKey('soapheader')) { // SP-initiated SOAP with soapheader LOG.debug("found soapheader parameter for SP-initiated") String message = inargs.get('soapheader') dispatchMessage(i2s, message) return } if (parameters.get('spInitiated') == 'true' && inargs.containsKey('')) { // SP-initiated SOAP with empty LOG.debug("found empty parameter for SP-initiated SOAP message") String message = inargs.get('') dispatchMessage(i2s, message) return } String issuer = inargs['Issuer'] ?: inargs['issuer'] if (parameters.get('idpInitiated') == 'true' && issuer != null) { // IDP-initiated authentication LOG.debug("found Issuer parameter for IDP-initiated authentication") dispatchIssuer(i2s, issuer) return } // used as fallback in case of ?logout (we need an IdentityProviderState) if (inargs.containsKey("logout") && session.containsKey('saml.idp.result')) { def result = session.get('saml.idp.result') LOG.debug("dispatching to last used ResultCond: $result") response.setResult(result) return } def location = getReferer('/') LOG.info("Unable to dispatch request. Giving up and redirecting (back) to $location") redirect(location)