Saturday, August 6, 2016

AEM - Processing SAML Response

Brief Note



There might be lot of articles which you might have read by now on the how to set up SAML authentication in AEM, configuring various options on AEM and on IDP provider side.

Few articles which I have read myself and enjoy reading it are listed below.


Useful References:

https://helpx.adobe.com/experience-manager/kb/saml-demo.html
http://www.aemstuff.com/blogs/july/saml.html
http://adobeaemclub.com/setting-saml-authentication/


Important information which I did not get while doing study was on how to process the SAML response which is received and what are the details which should be accounted for while implementing the same.

This article covers some of the findings which I had gone through in my study, some lesson learnt and things to look out for.



Processing SAML response using Authentication Info Post Processor


package com.ag.blog.agblog.core.postprocessors;

import java.io.IOException;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

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.lang3.StringUtils;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.auth.core.spi.AuthenticationInfo;
import org.apache.sling.auth.core.spi.AuthenticationInfoPostProcessor;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;


@Component(immediate = true, metatype = false)
@Service
public class SAMLResponsePostProcessor implements AuthenticationInfoPostProcessor {

private static final Logger LOGGER = LoggerFactory.getLogger(SAMLResponsePostProcessor.class);

@Reference
private SlingSettingsService slingSettingsService;

public void postProcess(AuthenticationInfo info, HttpServletRequest request, HttpServletResponse response)
throws LoginException {
HttpServletResponse httpResponse = null;
HttpServletRequest httpRequest = null;
try {
LOGGER.info("SAMLResponse Post Processor invoked");
httpResponse = response;
httpRequest = request;
String pathInfo = httpRequest.getPathInfo();
Set<String> runModes = slingSettingsService.getRunModes();
if (runModes.contains("publish") && StringUtils.isNotEmpty(pathInfo)
&& pathInfo.contains("saml_login")) {
LOGGER.info("SAMLResponse Post Processor processing ...");
String responseSAMLMessage = httpRequest.getParameter("saml_login");
if (StringUtils.isNotEmpty(responseSAMLMessage)) {
LOGGER.debug("responseSAMLMessage:" + responseSAMLMessage);
String base64DecodedResponse = decodeStr(responseSAMLMessage);
LOGGER.debug("base64DecodedResponse:" + base64DecodedResponse);
parseSAMLResponse(httpResponse, httpRequest, runModes, base64DecodedResponse);
} else {
LOGGER.info("responseSAMLMessage is empty or null");
}
}
} catch (ParserConfigurationException e) {
LOGGER.error("Unable to get Document Builder ", e);
} catch (SAXException e) {
LOGGER.error("Unable to parse the xml document ", e);
} catch (IOException e) {
LOGGER.error("IOException ", e);
}
}

/**
* This method will parse the SAML response to create the Cookie by reading the attributes.

* @param httpResponse
* @param httpRequest
* @param runModes
* @param base64DecodedResponse
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
* @throws UnsupportedEncodingException
*/
private void parseSAMLResponse(HttpServletResponse httpResponse, HttpServletRequest httpRequest,
Set<String> runModes, String base64DecodedResponse)
throws ParserConfigurationException, SAXException, IOException, UnsupportedEncodingException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
Map<String, String> samlAttributeMap = new HashMap<String, String>();
StringReader strReader = new StringReader(base64DecodedResponse);
InputSource inputSource = new InputSource(strReader);
Document document = docBuilder.parse(inputSource);
NodeList samlAssertion = document.getElementsByTagName("saml:Assertion");
populateSAMLAttrMap(samlAttributeMap, samlAssertion);
}

/**
* This method would populate the SAML attribute map object based on the attributes present in the response.

* @param samlAttributeMap
* @param samlAssertion
*/
private void populateSAMLAttrMap(Map<String, String> samlAttributeMap, NodeList samlAssertion) {
for (int i = 0; i < samlAssertion.getLength(); i++) {
Node item = samlAssertion.item(i);
NodeList childNodes = item.getChildNodes();
for (int j = 0; j < childNodes.getLength(); j++) {
Node subChildNode = childNodes.item(j);
if (subChildNode.getNodeName().equalsIgnoreCase("saml:AttributeStatement")) {
NodeList childNodes2 = subChildNode.getChildNodes();
for (int k = 0; k < childNodes2.getLength(); k++) {
Node item2 = childNodes2.item(k);
if (item2.getNodeName().equalsIgnoreCase("saml:Attribute")) {
String attributeValue = item2.getAttributes().item(0).getNodeValue();
NodeList attributeValueNodeList = item2.getChildNodes();
for (int l = 0; l < attributeValueNodeList.getLength(); l++) {
if (attributeValueNodeList.item(l).getNodeName()
.equalsIgnoreCase("saml:AttributeValue")) {
samlAttributeMap.put(attributeValue,
attributeValueNodeList.item(l).getTextContent());
}
}
}
}
}
}
}
}

/**
* This method would decode the SAML response.

* @param encodedStr
* @return
*/
public static String decodeStr(
String encodedStr) {
String decodedXML = "";
org.apache.commons.codec.binary.Base64 base64Decoder = new org.apache.commons.codec.binary.Base64();
byte[] xmlBytes = encodedStr.getBytes();
byte[] base64DecodedByteArray = base64Decoder.decode(xmlBytes);
decodedXML = new String(base64DecodedByteArray);
return decodedXML;
}

}






Lesson Learnt and things to look out for in the investigation of SAML response


Misunderstanding encoded response with encrypted response



Sometimes you would get confused between the encoded SAML response with the encrypted SAML response.

To always verify that, use the below link and paste the SAML response on the same. Select "post" option and click on decode it button.

This will show you the decoded response. Verify if all the attributes sent from the IDP provider is present or not.

https://idp.ssocircle.com/sso/toolbox/samlDecode.jsp



SAML response posting forbidden error



When IDP posts the SAML response to AEM, the IDP provider would configure the domain name with the context path containing /saml_login.

As we know that the domain would be configured to point to LB from Akamai CDN (if used) then to dispatchers and dispatchers to AEM publish instances.

Like Movie where predictions are done by all of us while watching it, likewise, people now might be predicting that some kind of dispatcher configurations needs to be done to hand over the SAML response to reach AEM instance.

Correct, in the dispatcher configuration file go ahead and add below rule to connect dispatcher and AEM for SAML response.

/filter
{
 ....

 ....
/0055 { /type "allow" /url "/saml_login" }

.....
.....
}


Prediction is not ending here, people who know "Apache Sling Servlet/Script Resolver and Error Handler" will think of allowing "saml_login" context path.

Yes, Go to configuration manager in the web console of the instance and add the context path "saml_login".

This is the manual change which I am showing. In one of my article you would also see how you can make this part of your code base.







SAML Response can be read only once in the life cycle


Lot of times you would think that you can use normal filters to take care of the reading of SAML response, but eventually you would end up having null pointer exceptions since the filters get executed twice in the life cycle.

SAML response can be read only once since the "saml_login" parameter which will hold the response will be read in an special class from one of the bundle in AEM which is ""com.adobe.granite.auth.saml.binding.PostBinding" class.

Filters would not work for this reason and you need to go ahead with Post processors to read it once PostBinding class which is at the same level of ranking to read the SAML response before response is set to NULL.




Intent of "saml_request_path" cookie



Lot of times you might need to land to the same page in AEM where you have clicked on the login link and this is achievable if you read the value from the "saml_request_path" cookie on page load or from back end when the control comes back to AEM instance from the IDP server.


This "saml_request_path" cookie is created by the class "com.adobe.granite.auth.saml.SamlAuthenticationHandler" in the method requestCredentials().


There would be implementations where you feel like hitting the respective environment IDP URL on click of login, during this time make sure that you have a logic in your code to create the "saml_request_path" cookie with the path set to the current page by reading from the request object (or any other logic).



2 comments:

  1. This helped me a lot. Thanks for sharing this. But I have a different requirement. The website domain cant be accessed directly. The access should come from a link from another website. In our website we need to get the post param details and SAML request should be initiated and user need to be authenticated. Can you help me with any suggestion on the implementation of SAML request generation.

    ReplyDelete
  2. Hi Apoorva,

    Where can I set the saml_request_path in AEM 6.2? Is it a OSGI change? If yes, then where an I find?

    Thanks,
    Jineet

    ReplyDelete