Wednesday, July 23, 2014

WSO2 IS SAML SSO using openSAML java library

In this blog post i will explain how to write a sample SSO service provider code using wso2is as the identity provider and openSAML as the supporting library to create SAML request.


First download the wso2IS from [1] and openSAML release from [2]. Then startup the wso2is. Create a new java web application project and import below jars from opensaml lib and lib/endorsed folders.


    serializer-2.10.0
    xalan-2.7.1
    xercesImpl-2.10.0
    xml-apis-2.10.0
    xmltooling-1.4.1
    xmlsec-1.5.6
    slf4j-api-1.7.5
    openws-1.5.1
    opensaml-2.6.1
    joda-time-2.2
    esapi-2.0.1
    commons-lang-2.6


Also include “axiom-api.jar” from apache axis project which has the UIDGenerator.java class.


Lets take our service provider name is “test.com” & our consumer landing page is “http://localhost:8080/test.com/home.jsp”. (You have to register a new service provider with above fields in wso2 identity server. To do so please refer [3] )


You can use below code to sent login & logout saml requests.




import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.joda.time.DateTime;
import org.opensaml.Configuration;
import org.opensaml.DefaultBootstrap;
import org.opensaml.common.SAMLVersion;
import org.opensaml.saml2.core.Assertion;
import org.opensaml.saml2.core.Attribute;
import org.opensaml.saml2.core.AttributeStatement;
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.LogoutRequest;
import org.opensaml.saml2.core.NameID;
import org.opensaml.saml2.core.NameIDPolicy;
import org.opensaml.saml2.core.RequestAbstractType;
import org.opensaml.saml2.core.RequestedAuthnContext;
import org.opensaml.saml2.core.Response;
import org.opensaml.saml2.core.SessionIndex;
import org.opensaml.saml2.core.impl.AuthnContextClassRefBuilder;
import org.opensaml.saml2.core.impl.AuthnRequestBuilder;
import org.opensaml.saml2.core.impl.IssuerBuilder;
import org.opensaml.saml2.core.impl.LogoutRequestBuilder;
import org.opensaml.saml2.core.impl.NameIDBuilder;
import org.opensaml.saml2.core.impl.NameIDPolicyBuilder;
import org.opensaml.saml2.core.impl.RequestedAuthnContextBuilder;
import org.opensaml.saml2.core.impl.SessionIndexBuilder;
import org.opensaml.xml.ConfigurationException;
import org.opensaml.xml.XMLObject;
import org.opensaml.xml.XMLObjectBuilder;
import org.opensaml.xml.XMLObjectBuilderFactory;
import org.opensaml.xml.io.Marshaller;
import org.opensaml.xml.io.MarshallingException;
import org.opensaml.xml.io.Unmarshaller;
import org.opensaml.xml.io.UnmarshallerFactory;
import org.opensaml.xml.io.UnmarshallingException;
import org.opensaml.xml.util.Base64;
import org.opensaml.xml.util.XMLHelper;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

public class SAMLConsumer {
    
    private static final Logger logger = LoggerFactory.getLogger(SAMLConsumer.class);
    private static boolean isBootstraped = false;

    private static final String ISSUER_URL = “test.com”;
    private static final String CONSUMER_URL = “http://localhost:8080/test.com/home.jsp”;

    private static final String NAME_SPACE_URI_ISSUER = “urn:oasis:names:tc:SAML:2.0:assertion”;
    private static final String NAME_SPACE_URI_AUTH = “urn:oasis:names:tc:SAML:2.0:protocol”;
    private static final String SAML_TRANSPORT_TYPE_PWD_PROTECTED = “urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport”;
    private static final String SAML_TRANSPORT_PROTOCOL_BINDING = “urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST”;
    
    private static final String LOCAL_NAME_ISSUER = “Issuer”;
    private static final String LOCAL_NAME_AUTHRQ = “AuthnRequest”;
    private static final String NAME_SPACE_PREFIX = “samlp”;
    private static final String AUTH_CNTXT_REF = “AuthnContextClassRef”;
    private static final String SAML = “saml”;
    
    public static String buildLoginRequest() throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException, MarshallingException, IOException  {
        logger.info(“started”);

        IssuerBuilder issuerBuilder = new IssuerBuilder();
        Issuer issuer = issuerBuilder.buildObject(NAME_SPACE_URI_ISSUER, LOCAL_NAME_ISSUER, “samlp”);
        issuer.setValue(ISSUER_URL);
        
        NameIDPolicyBuilder nameIdPolicyBuilder = new NameIDPolicyBuilder();
        NameIDPolicy nameIdPolicy = nameIdPolicyBuilder.buildObject();
        nameIdPolicy.setFormat(“urn:oasis:names:tc:SAML:2.0:nameid-format:persistent”);
        nameIdPolicy.setSPNameQualifier(“Isser”);
        nameIdPolicy.setAllowCreate(new Boolean(true));
        
        AuthnContextClassRefBuilder authnContextClassRefBuilder = new AuthnContextClassRefBuilder();
        AuthnContextClassRef authnContextClassRef =
                                                    authnContextClassRefBuilder.buildObject(NAME_SPACE_URI_ISSUER,
                                                            AUTH_CNTXT_REF, SAML);
        authnContextClassRef.setAuthnContextClassRef(SAML_TRANSPORT_TYPE_PWD_PROTECTED);
        
        RequestedAuthnContextBuilder requestedAuthnContextBuilder =
                                                        new RequestedAuthnContextBuilder();
        RequestedAuthnContext requestedAuthnContext = requestedAuthnContextBuilder.buildObject();
        requestedAuthnContext.setComparison(AuthnContextComparisonTypeEnumeration.EXACT);
        requestedAuthnContext.getAuthnContextClassRefs().add(authnContextClassRef);
        
        DateTime issueInstant = new DateTime();
        AuthnRequestBuilder authnRequestBuilder = new AuthnRequestBuilder();
        AuthnRequest authnRequest = authnRequestBuilder.buildObject(NAME_SPACE_URI_AUTH, LOCAL_NAME_AUTHRQ, NAME_SPACE_PREFIX);
        authnRequest.setForceAuthn(new Boolean(false));
        authnRequest.setIsPassive(new Boolean(false));
        authnRequest.setIssueInstant(issueInstant);
        authnRequest.setProtocolBinding(SAML_TRANSPORT_PROTOCOL_BINDING);
        authnRequest.setAssertionConsumerServiceURL(CONSUMER_URL);
        authnRequest.setIssuer(issuer);
        authnRequest.setNameIDPolicy(nameIdPolicy);
        authnRequest.setRequestedAuthnContext(requestedAuthnContext);
        
        String authReqRandomId = Integer.toHexString(new Double(Math.random()).intValue());
        
        authnRequest.setID(authReqRandomId);
        authnRequest.setVersion(SAMLVersion.VERSION_20);
                
        return marshall(authnRequest);
        
    }
    
    public static String buildLogoutRequest(String user) throws MarshallingException, IOException {

        LogoutRequest logoutReq = new LogoutRequestBuilder().buildObject();

        logoutReq.setID(createID());

        DateTime issueInstant = new DateTime();
        logoutReq.setIssueInstant(issueInstant);
        logoutReq.setNotOnOrAfter(new DateTime(issueInstant.getMillis() + 5 * 60 * 1000));

        IssuerBuilder issuerBuilder = new IssuerBuilder();
        Issuer issuer = issuerBuilder.buildObject();
        issuer.setValue(ISSUER_URL);
        logoutReq.setIssuer(issuer);

        NameID nameId = new NameIDBuilder().buildObject();
        nameId.setFormat(“urn:oasis:names:tc:SAML:2.0:nameid-format:entity”);
        nameId.setValue(user);
        logoutReq.setNameID(nameId);

        SessionIndex sessionIndex = new SessionIndexBuilder().buildObject();
        sessionIndex.setSessionIndex(UIDGenerator.generateUID());
        logoutReq.getSessionIndexes().add(sessionIndex);

        logoutReq.setReason(“Single Logout”);

        return marshall(logoutReq);
    }
    
    private static String marshall(RequestAbstractType authnRequest) throws MarshallingException, IOException {
        doBootstrap();
        Marshaller marshaller = Configuration.getMarshallerFactory().getMarshaller(authnRequest);
        Element authDOM = marshaller.marshall(authnRequest);
        
        
        StringWriter rspWrt = new StringWriter();
        XMLHelper.writeNode(authDOM, rspWrt);
        String requestMessage = rspWrt.toString();
        
        System.out.println(requestMessage);
                 
        Deflater deflater = new Deflater(Deflater.DEFLATED, true);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(byteArrayOutputStream, deflater);
        deflaterOutputStream.write(requestMessage.getBytes());
        deflaterOutputStream.close();
                 
        /* Encoding the compressed message */
        String encodedRequestMessage = Base64.encodeBytes(byteArrayOutputStream.toByteArray(), Base64.DONT_BREAK_LINES);
        String encodedAuthnRequest = URLEncoder.encode(encodedRequestMessage,”UTF-8”).trim();;
        
        return encodedAuthnRequest;
    }
    
    
    public static <T> T createSAMLObject(final Class<T> clazz) throws IllegalArgumentException, SecurityException, IllegalAccessException, NoSuchFieldException {
         XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory();
        
         QName defaultElementName = (QName)clazz.getDeclaredField(“DEFAULT_ELEMENT_NAME”).get(null);
         Map<QName, XMLObjectBuilder> builderMap= builderFactory.getBuilders();
         System.out.println(“is nul “  + builderMap.get(defaultElementName));
        
        return null;
    }
    
    private static void doBootstrap() {
        if(!isBootstraped) {
            try {
                DefaultBootstrap.bootstrap();
                isBootstraped = true;
            } catch (ConfigurationException e) {
                logger.error(“Error calling bootstrap”, e);
            }
        }
    }
    
    public static Map<String, String> processResponse(String response) {

        XMLObject resp = null;
        
        try {
            resp = unmarshall(response);
        } catch (ConfigurationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (SAXException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (UnmarshallingException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return getResult(resp);
        
    }
    
    private static XMLObject unmarshall(String responseMessage) throws ConfigurationException,
                            ParserConfigurationException, SAXException, IOException, UnmarshallingException {
        
        doBootstrap();
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        documentBuilderFactory.setNamespaceAware(true);
        DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
        
        byte[] base64DecodedResponse = Base64.decode(responseMessage.trim());
        
        System.out.println(new String(base64DecodedResponse));
        
        ByteArrayInputStream is = new ByteArrayInputStream(base64DecodedResponse);
        
        Document document = docBuilder.parse(is);
        Element element = document.getDocumentElement();
        UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
        Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
        return unmarshaller.unmarshall(element);
        
    }
    
    private static Map<String, String> getResult(XMLObject responseXmlObj) {

        if (responseXmlObj.getDOM().getNodeName().equals(“saml2p:LogoutResponse”)) {
            logger.error(“user logout”);
            return null;
        }

        Response response = (Response) responseXmlObj;
        logger.info(“SAML resp” + response);

        Assertion assertion = response.getAssertions().get(0);
        Map<String, String> resutls = new HashMap<String, String>();

        /*
         * If the request has failed, the IDP shouldn’t send an assertion.
         * SSO profile spec 4.1.4.2 <Response> Usage
         */
        if (assertion != null) {

            String subject = assertion.getSubject().getNameID().getValue();
            resutls.put(“Subject”, subject); // get the subject

            List<AttributeStatement> attributeStatementList = assertion.getAttributeStatements();

            if (attributeStatementList != null) {
                // we have received attributes of user
                Iterator<AttributeStatement> attribStatIter = attributeStatementList.iterator();
                while (attribStatIter.hasNext()) {
                    AttributeStatement statment = attribStatIter.next();
                    List<Attribute> attributesList = statment.getAttributes();
                    Iterator<Attribute> attributesIter = attributesList.iterator();
                    while (attributesIter.hasNext()) {
                        Attribute attrib = attributesIter.next();
                        Element value = attrib.getAttributeValues().get(0).getDOM();
                        String attribValue = value.getTextContent();
                        resutls.put(attrib.getName(), attribValue);
                    }
                }
            }
        }
        return resutls;
    }
    
    public static String createID() {

        byte[] bytes = new byte[20]; // 160 bit
        
        new Random().nextBytes(bytes);
        
        char[] charMapping = {‘a’, ‘b’, ‘c’, ‘d’, ‘e’, ‘f’, ‘g’, ‘h’, ‘i’, ‘j’, ‘k’, ‘l’, ‘m’, ‘n’, ‘o’, ‘p’};

        char[] chars = new char[40];

        for (int i = 0; i < bytes.length; i++) {
            int left = (bytes[i] » 4) & 0x0f;
            int right = bytes[i] & 0x0f;
            chars[i * 2] = charMapping[left];
            chars[i * 2 + 1] = charMapping[right];
        }

        return String.valueOf(chars);
    }
    
}




[1] http://wso2.com/products/identity-server/


[2] https://wiki.shibboleth.net/confluence/display/OpenSAML/Home


[3] http://pavithramadurangi.blogspot.com/2013/09/saml-20-sso-with-wso2-is-450.html