org.guanxi.idp.service.SSOBase.java Source code

Java tutorial

Introduction

Here is the source code for org.guanxi.idp.service.SSOBase.java

Source

//: "The contents of this file are subject to the Mozilla Public License
//: Version 1.1 (the "License"); you may not use this file except in
//: compliance with the License. You may obtain a copy of the License at
//: http://www.mozilla.org/MPL/
//:
//: Software distributed under the License is distributed on an "AS IS"
//: basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//: License for the specific language governing rights and limitations
//: under the License.
//:
//: The Original Code is Guanxi (http://www.guanxi.uhi.ac.uk).
//:
//: The Initial Developer of the Original Code is Alistair Young alistair@codebrane.com
//: All Rights Reserved.
//:

package org.guanxi.idp.service;

import org.apache.xml.security.encryption.EncryptedData;
import org.apache.xml.security.encryption.EncryptedKey;
import org.apache.xml.security.encryption.XMLCipher;
import org.apache.xml.security.encryption.XMLEncryptionException;
import org.apache.xml.security.keys.KeyInfo;
import org.bouncycastle.openssl.PEMWriter;
import org.guanxi.common.GuanxiPrincipal;
import org.guanxi.common.Utils;
import org.guanxi.xal.idp.*;
import org.guanxi.xal.saml_2_0.metadata.EntityDescriptorType;
import org.guanxi.xal.saml_2_0.metadata.KeyDescriptorType;
import org.guanxi.xal.saml_2_0.metadata.KeyTypes;
import org.guanxi.xal.saml_2_0.assertion.*;
import org.guanxi.common.definitions.Shibboleth;
import org.guanxi.common.definitions.SAML;
import org.guanxi.common.definitions.Guanxi;
import org.guanxi.common.definitions.EduPersonOID;
import org.guanxi.common.entity.EntityFarm;
import org.guanxi.common.entity.EntityManager;
import org.guanxi.common.metadata.SPMetadata;
import org.guanxi.common.GuanxiException;
import org.guanxi.common.security.SecUtilsConfig;
import org.guanxi.idp.util.AttributeMap;
import org.guanxi.idp.util.ARPEngine;
import org.guanxi.idp.farm.attributors.Attributor;
import org.guanxi.xal.saml_2_0.protocol.*;
import org.guanxi.xal.w3.xmldsig.KeyInfoDocument;
import org.guanxi.xal.w3.xmldsig.X509DataType;
import org.guanxi.xal.w3.xmlenc.EncryptedDataDocument;
import org.guanxi.xal.w3.xmlenc.EncryptedDataType;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.context.ServletContextAware;
import org.springframework.context.MessageSource;
import org.apache.log4j.Logger;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.XmlObject;
import org.w3c.dom.*;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.Calendar;
import java.util.HashMap;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;
import java.io.ByteArrayInputStream;

/**
 * Base class for SSO profile handlers
 * 
 * @author alistair
 */
public abstract class SSOBase extends AbstractController implements ServletContextAware {
    protected static final String ENTITY_IDP = "ENTITY_IDP";
    protected static final String ENTITY_SP = "ENTITY_SP";
    protected static final String SIGNING_CERT = "SIGNING_CERT";
    protected static final String ENCRYPTION_CERT = "ENCRYPTION_CERT";

    /** Our logger */
    protected Logger logger = null;
    /** The localised messages to use */
    protected MessageSource messages = null;
    /** Our config */
    protected IdpDocument.Idp idpConfig;
    /** The name of the default SP entry in the config file to use */
    protected String defaultSPEntry = null;
    /** The number of seconds assertions should be valid for */
    protected int assertionTimeLimit;
    /** The entityID to use when sending a SAML Response */
    protected String idpEntityID = null;
    /** The name qualifier to use when sending a SAML Response */
    protected String nameQualifier = null;
    /** The name qualifier format to use when sending a SAML Response */
    protected String nameQualifierFormat = null;
    /** The signing credentials to use */
    protected Creds credsConfig = null;
    /** The security information to use when signing a response */
    protected SecUtilsConfig secUtilsConfig = null;
    /** SAML2 namesapces for writing SAML */
    protected HashMap<String, String> saml2Namespaces = null;
    /** XML output options */
    protected XmlOptions xmlOptions = null;
    /** Our profile specific attribute mapper */
    protected AttributeMap mapper = null;
    /** Our ARP engine */
    protected ARPEngine arpEngine = null;
    /** The Attributors to use to get attributes */
    protected org.guanxi.idp.farm.attributors.Attributor[] attributor = null;

    public void init() {
        logger = Logger.getLogger(this.getClass().getName());
        saml2Namespaces = new HashMap<String, String>();
        saml2Namespaces.put(SAML.NS_SAML_20_PROTOCOL, SAML.NS_PREFIX_SAML_20_PROTOCOL);
        saml2Namespaces.put(SAML.NS_SAML_20_ASSERTION, SAML.NS_PREFIX_SAML_20_ASSERTION);
        xmlOptions = new XmlOptions();
        xmlOptions.setSavePrettyPrint();
        xmlOptions.setSavePrettyPrintIndent(2);
        xmlOptions.setUseDefaultNamespace();
        xmlOptions.setSaveAggressiveNamespaces();
        xmlOptions.setSaveNamespacesFirst();
    }

    /**
     * Handles processing of an AuthnRequest message. If the request attribute:
     * wbsso-handler-error-message
     * is present then we must display the error text it contains and go no further.
     *
     * @param request Servlet request
     * @param response Servlet response
     * @return the view to display
     * @throws Exception if an error occurs
     */
    public abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
            throws Exception;

    /**
     * Loads the identity and credential information based on the relying party.
     * These determine what entityID and signing credentials to use.
     *
     * @param request Servlet request
     */
    protected void loadPersona(HttpServletRequest request) {
        /* Now load the appropriate identity and creds from the config file.
         * We'll either use the default or the ones that the particular SP
         * needs to be sent.
         */
        String spID = null;
        ServiceProvider[] spList = idpConfig.getServiceProviderArray();
        for (int c = 0; c < spList.length; c++) {
            if (spList[c].getName().equals(request.getParameter(Shibboleth.PROVIDER_ID))) {
                spID = request.getParameter(Shibboleth.PROVIDER_ID);
            }
        }
        if (spID == null) {
            // No specific requirement for this SP so use the default identity and creds
            spID = defaultSPEntry;
        }

        // Now we've sorted the SP id to use, load the identity and creds
        for (int c = 0; c < spList.length; c++) {
            if (spList[c].getName().equals(spID)) {
                String identityToUse = spList[c].getIdentity();
                String credsToUse = spList[c].getCreds();

                // We've found the <service-provider> node so look for the corresponding <identity> node
                org.guanxi.xal.idp.Identity[] ids = idpConfig.getIdentityArray();
                for (int cc = 0; cc < ids.length; cc++) {
                    if (ids[cc].getName().equals(identityToUse)) {
                        idpEntityID = ids[cc].getIssuer();
                        nameQualifier = ids[cc].getNameQualifier();
                        nameQualifierFormat = ids[cc].getFormat();
                    }
                }

                // Look for the corresponding <creds> node
                org.guanxi.xal.idp.Creds[] creds = idpConfig.getCredsArray();
                for (int ccc = 0; ccc < creds.length; ccc++) {
                    if (creds[ccc].getName().equals(credsToUse)) {
                        credsConfig = creds[ccc];
                    }
                }
            }
        }

        // Initialise the signing information
        secUtilsConfig = new SecUtilsConfig();
        secUtilsConfig.setKeystoreFile(credsConfig.getKeystoreFile());
        secUtilsConfig.setKeystorePass(credsConfig.getKeystorePassword());
        secUtilsConfig.setKeystoreType("JKS");
        secUtilsConfig.setPrivateKeyAlias(credsConfig.getPrivateKeyAlias());
        secUtilsConfig.setPrivateKeyPass(credsConfig.getPrivateKeyPassword());
        secUtilsConfig.setCertificateAlias(credsConfig.getCertificateAlias());
    }

    /**
     * Loads a Service Provider's metadata from the store.
     *
     * @param spEntityID the SP's entityID
     * @return EntityDescriptorType for the SP
     */
    protected EntityDescriptorType getSPMetadata(String spEntityID) {
        EntityFarm farm = (EntityFarm) getServletContext().getAttribute(Guanxi.CONTEXT_ATTR_IDP_ENTITY_FARM);
        EntityManager manager = farm.getEntityManagerForID(spEntityID);
        SPMetadata metadata = (SPMetadata) manager.getMetadata(spEntityID);
        return (EntityDescriptorType) metadata.getPrivateData();
    }

    /**
     * Extracts the appropriate X509 certificate from an entity's SAML2 metadata
     *
     * @param saml2Metadata the entity's SAML2 metadata
     * @param entityType the type of the entity. ENTITY_IDP or ENTITY_SP
     * @param certType the type of cert to get. SIGNING_CERT or ENCRYPTION_CERT
     * @return the X509Certificate or null if it doesn't recognise a param
     * @throws GuanxiException if an error occurs
     */
    protected X509Certificate getX509CertFromMetadata(EntityDescriptorType saml2Metadata, String entityType,
            String certType) throws GuanxiException {
        try {
            KeyTypes.Enum keyType;
            if (certType.equals(SIGNING_CERT)) {
                keyType = KeyTypes.SIGNING;
            } else if (certType.equals(ENCRYPTION_CERT)) {
                keyType = KeyTypes.ENCRYPTION;
            } else {
                return null;
            }

            KeyDescriptorType[] keyDescriptors = null;
            if (entityType.equals(ENTITY_IDP)) {
                keyDescriptors = saml2Metadata.getIDPSSODescriptorArray(0).getKeyDescriptorArray();
            } else if (entityType.equals(ENTITY_SP)) {
                keyDescriptors = saml2Metadata.getSPSSODescriptorArray(0).getKeyDescriptorArray();
            } else {
                return null;
            }

            for (KeyDescriptorType keyDescriptor : keyDescriptors) {
                if (keyDescriptor.getUse() != null) {
                    if (keyDescriptor.getUse().equals(keyType)) {
                        return getCertFromKeyDescriptor(keyDescriptor);
                    }
                }
            }

            /* If we get here there are no keys specifically marked for this type of usage
             * so use the first one in the list. Or if there are none, give up!
             */
            if ((keyDescriptors == null) || (keyDescriptors.length == 0)) {
                return null;
            } else {
                return getCertFromKeyDescriptor(keyDescriptors[0]);
            }
        } catch (Exception e) {
            throw new GuanxiException(e);
        }
    }

    /**
     * Create a SAML2 AttributeStatement based on the attributes released from an Attributor farm
     *
     * @param guanxiAttrFarmOutput the attributes released from an Attributor farm
     * @param entityID the entityID of the relying party that wants the attributes
     * @return AttributeStatementDocument containing the AttributeStatement
     */
    protected AttributeStatementDocument getSAML2AttributeStatementFromFarm(
            UserAttributesDocument guanxiAttrFarmOutput, String entityID, SubjectType subject,
            GuanxiPrincipal gxPrincipal) {
        AttributeStatementDocument attrStatementDoc = AttributeStatementDocument.Factory.newInstance();
        AttributeStatementType attrStatement = attrStatementDoc.addNewAttributeStatement();

        boolean hasAttrs = false;
        for (int c = 0; c < guanxiAttrFarmOutput.getUserAttributes().getAttributeArray().length; c++) {
            AttributorAttribute attributorAttr = guanxiAttrFarmOutput.getUserAttributes().getAttributeArray(c);

            // Has the attribute already been processed? i.e. does it have multiple values?
            AttributeType attribute = null;
            AttributeType[] existingAttrs = attrStatement.getAttributeArray();
            if (existingAttrs != null) {
                for (int cc = 0; cc < existingAttrs.length; cc++) {
                    if (existingAttrs[cc].getName().equals(attributorAttr.getName())) {
                        attribute = existingAttrs[cc];
                    }
                }
            }

            // New attribute, not yet processed
            XmlObject attrValue = null;
            if (attribute == null) {
                if (attributorAttr.getName().equalsIgnoreCase("__NAMEID__")) {
                    NameIDType nameID = subject.addNewNameID();
                    nameID.setFormat(gxPrincipal.getNameIDFormat());
                    nameID.setStringValue(gxPrincipal.getName());
                } else {
                    hasAttrs = true;
                    attribute = attrStatement.addNewAttribute();
                    attribute.setName(EduPersonOID.ATTRIBUTE_NAME_PREFIX + attributorAttr.getName());
                    if (attributorAttr.getFriendlyName() != null) {
                        attribute.setFriendlyName(attributorAttr.getFriendlyName());
                    }
                    attribute.setNameFormat(SAML.SAML2_ATTRIBUTE_NAME_FORMAT_URI);
                    attrValue = attribute.addNewAttributeValue();
                }
            }

            // Deal with scoped eduPerson attributes
            if (attribute != null) {
                if (attribute.getName()
                        .equals(EduPersonOID.ATTRIBUTE_NAME_PREFIX + EduPersonOID.OID_EDUPERSON_TARGETED_ID)) {
                    NameIDDocument nameIDDoc = NameIDDocument.Factory.newInstance();
                    NameIDType nameID = nameIDDoc.addNewNameID();
                    nameID.setFormat(SAML.SAML2_ATTRIBUTE_FORMAT_NAMEID_PERSISTENT);
                    nameID.setNameQualifier(nameQualifier);
                    nameID.setSPNameQualifier(entityID);
                    // For SAML2 we need to remove the scope from the value
                    if (attributorAttr.getValue().contains("@")) {
                        attributorAttr.setValue(attributorAttr.getValue().split("@")[0]);
                    }
                    nameID.setStringValue(attributorAttr.getValue());

                    attrValue.getDomNode().appendChild(
                            attrValue.getDomNode().getOwnerDocument().importNode(nameID.getDomNode(), true));
                } else {
                    Text valueNode = attrValue.getDomNode().getOwnerDocument()
                            .createTextNode(attributorAttr.getValue());
                    attrValue.getDomNode().appendChild(valueNode);
                }
            }
        } // for (int c=0; c < guanxiAttrFarmOutput.getUserAttributes().getAttributeArray().length; c++)

        if (hasAttrs)
            return attrStatementDoc;
        else
            return null;
    }

    /**
     * Adds encrypted assertions to a SAML2 Response
     *
     * @param encryptionCert the X509 certificate to use for encrypting the assertions
     * @param assertionDoc the assertions to encrypt
     * @param responseDoc the SAML2 Response to add the encrypted assertions to
     * @throws GuanxiException if an error occurs
     */
    protected void addEncryptedAssertionsToResponse(X509Certificate encryptionCert, AssertionDocument assertionDoc,
            ResponseDocument responseDoc) throws GuanxiException {
        try {
            PublicKey keyEncryptKey = encryptionCert.getPublicKey();

            // Generate a secret key
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(128);
            SecretKey secretKey = keyGenerator.generateKey();

            XMLCipher keyCipher = XMLCipher.getInstance(XMLCipher.RSA_OAEP);
            keyCipher.init(XMLCipher.WRAP_MODE, keyEncryptKey);

            Document domAssertionDoc = (Document) assertionDoc.newDomNode(xmlOptions);
            EncryptedKey encryptedKey = keyCipher.encryptKey(domAssertionDoc, secretKey);

            Element elementToEncrypt = domAssertionDoc.getDocumentElement();

            XMLCipher xmlCipher = XMLCipher.getInstance(XMLCipher.AES_128);
            xmlCipher.init(XMLCipher.ENCRYPT_MODE, secretKey);

            // Add KeyInfo to the EncryptedData element
            EncryptedData encryptedDataElement = xmlCipher.getEncryptedData();
            KeyInfo keyInfo = new KeyInfo(domAssertionDoc);
            keyInfo.add(encryptedKey);
            encryptedDataElement.setKeyInfo(keyInfo);

            // Encrypt the assertion
            xmlCipher.doFinal(domAssertionDoc, elementToEncrypt, false);

            // Go back into XMLBeans land...
            EncryptedDataDocument encryptedDataDoc = EncryptedDataDocument.Factory.parse(domAssertionDoc);
            // ...and add the encrypted assertion to the response
            responseDoc.getResponse().addNewEncryptedAssertion()
                    .setEncryptedData(encryptedDataDoc.getEncryptedData());

            // Look for the Response/EncryptedAssertion/EncryptedData/KeyInfo/EncryptedKey node...
            EncryptedDataType encryptedData = responseDoc.getResponse().getEncryptedAssertionArray(0)
                    .getEncryptedData();
            NodeList nodes = encryptedData.getKeyInfo().getDomNode().getChildNodes();
            Node encryptedKeyNode = null;
            for (int c = 0; c < nodes.getLength(); c++) {
                encryptedKeyNode = nodes.item(c);
                if (encryptedKeyNode.getLocalName() != null) {
                    if (encryptedKeyNode.getLocalName().equals("EncryptedKey"))
                        break;
                }
            }

            // ...get a new KeyInfo ready...
            KeyInfoDocument keyInfoDoc = KeyInfoDocument.Factory.newInstance();
            X509DataType x509Data = keyInfoDoc.addNewKeyInfo().addNewX509Data();

            // ...and a useable version of the SP's encryption certificate...
            StringWriter sw = new StringWriter();
            PEMWriter pemWriter = new PEMWriter(sw);
            pemWriter.writeObject(encryptionCert);
            pemWriter.close();
            String x509 = sw.toString();
            x509 = x509.replaceAll("-----BEGIN CERTIFICATE-----", "");
            x509 = x509.replaceAll("-----END CERTIFICATE-----", "");

            // ...add the encryption cert to the new KeyInfo...
            x509Data.addNewX509Certificate().setStringValue(x509);

            // ...and insert it into Response/EncryptedAssertion/EncryptedData/KeyInfo/EncryptedKey
            encryptedKeyNode.appendChild(
                    encryptedKeyNode.getOwnerDocument().importNode(keyInfoDoc.getKeyInfo().getDomNode(), true));
        } catch (NoSuchAlgorithmException nsae) {
            logger.error("AES encryption not available");
            throw new GuanxiException(nsae);
        } catch (XMLEncryptionException xea) {
            logger.error("RSA_OAEP error with WRAP_MODE");
            throw new GuanxiException(xea);
        } catch (Exception e) {
            logger.error("Error encyrpting the assertion");
            throw new GuanxiException(e);
        }
    }

    protected ResponseDocument buidErrorResponse(String entityID, String requestID, String acsURL,
            String statusCode) {
        // Response
        ResponseDocument responseDoc = ResponseDocument.Factory.newInstance();
        ResponseType wbssoResponse = responseDoc.addNewResponse();
        wbssoResponse.setID(generateStringID());
        wbssoResponse.setVersion("2.0");
        wbssoResponse.setDestination(acsURL);
        wbssoResponse.setIssueInstant(Calendar.getInstance());
        wbssoResponse.setInResponseTo(requestID);
        Utils.zuluXmlObject(wbssoResponse, 0);

        // Response/Issuer
        NameIDType issuer = wbssoResponse.addNewIssuer();
        issuer.setFormat(SAML.URN_SAML2_NAMEID_FORMAT_ENTITY);
        issuer.setStringValue(entityID);

        // Response/Status
        StatusDocument statusDoc = StatusDocument.Factory.newInstance();
        StatusType status = statusDoc.addNewStatus();
        StatusCodeType topLevelStatusCode = status.addNewStatusCode();
        topLevelStatusCode.setValue(SAML.SAML2_STATUS_RESPONDER);
        StatusCodeType secondLevelStatusCode = topLevelStatusCode.addNewStatusCode();
        secondLevelStatusCode.setValue(statusCode);
        wbssoResponse.setStatus(status);

        return responseDoc;
    }

    protected String generateStringID() {
        SecureRandom random = new SecureRandom();
        return "gx" + new BigInteger(130, random).toString(32);
    }

    /**
     * Extracts the X509 cenrtificate from a KeyDescriptor
     *
     * @param keyDescriptor the KeyDescriptor containing the X509 certificate
     * @return X509Certificate
     * @throws GuanxiException if an error occurs
     */
    private X509Certificate getCertFromKeyDescriptor(KeyDescriptorType keyDescriptor) throws GuanxiException {
        try {
            byte[] spCertBytes = keyDescriptor.getKeyInfo().getX509DataArray(0).getX509CertificateArray(0);
            CertificateFactory certFactory = CertificateFactory.getInstance("x.509");
            ByteArrayInputStream certByteStream = new ByteArrayInputStream(spCertBytes);
            X509Certificate metadataCert = (X509Certificate) certFactory.generateCertificate(certByteStream);
            certByteStream.close();
            return metadataCert;
        } catch (CertificateException ce) {
            logger.error("can't get x509 from KeyDescriptor");
            throw new GuanxiException(ce);
        } catch (IOException ioe) {
            logger.error("can't close cert byte stream");
            throw new GuanxiException(ioe);
        }
    }

    // Setters
    public void setDefaultSPEntry(String defaultSPEntry) {
        this.defaultSPEntry = defaultSPEntry;
    }

    public void setAssertionTimeLimit(int assertionTimeLimit) {
        this.assertionTimeLimit = assertionTimeLimit;
    }

    public void setMapper(AttributeMap mapper) {
        this.mapper = mapper;
    }

    public void setArpEngine(ARPEngine arpEngine) {
        this.arpEngine = arpEngine;
    }

    public void setAttributor(Attributor[] attributor) {
        this.attributor = attributor;
    }
}