Java tutorial
//: "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.shibboleth; import java.io.*; import java.math.BigInteger; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Calendar; import java.util.HashMap; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.xml.namespace.QName; import org.apache.log4j.Logger; import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.XmlOptions; import org.guanxi.common.GuanxiException; import org.guanxi.common.GuanxiPrincipal; import org.guanxi.common.Utils; import org.guanxi.common.definitions.*; import org.guanxi.common.entity.EntityFarm; import org.guanxi.common.entity.EntityManager; import org.guanxi.common.security.SecUtils; import org.guanxi.common.security.SecUtilsConfig; import org.guanxi.xal.idp.AttributorAttribute; import org.guanxi.xal.idp.IdpDocument; import org.guanxi.xal.idp.UserAttributesDocument; import org.guanxi.xal.idp.ServiceProvider; import org.guanxi.xal.saml_1_0.assertion.AssertionDocument; import org.guanxi.xal.saml_1_0.assertion.AssertionType; import org.guanxi.xal.saml_1_0.assertion.AttributeStatementDocument; import org.guanxi.xal.saml_1_0.assertion.AttributeStatementType; import org.guanxi.xal.saml_1_0.assertion.AttributeType; import org.guanxi.xal.saml_1_0.assertion.ConditionsDocument; import org.guanxi.xal.saml_1_0.assertion.ConditionsType; import org.guanxi.xal.saml_1_0.assertion.NameIdentifierType; import org.guanxi.xal.saml_1_0.assertion.SubjectType; import org.guanxi.xal.saml_1_0.protocol.RequestDocument; import org.guanxi.xal.saml_1_0.protocol.RequestType; import org.guanxi.xal.saml_1_0.protocol.ResponseDocument; import org.guanxi.xal.saml_1_0.protocol.ResponseType; import org.guanxi.xal.saml_1_0.protocol.StatusCodeType; import org.guanxi.xal.saml_1_0.protocol.StatusDocument; import org.guanxi.xal.saml_1_0.protocol.StatusType; import org.guanxi.xal.saml_2_0.assertion.NameIDDocument; import org.guanxi.xal.saml_2_0.assertion.NameIDType; import org.guanxi.xal.saml_2_0.metadata.EntityDescriptorType; import org.guanxi.xal.soap.Body; import org.guanxi.xal.soap.Envelope; import org.guanxi.xal.soap.EnvelopeDocument; import org.guanxi.idp.util.AttributeMap; import org.guanxi.idp.util.ARPEngine; import org.springframework.web.context.ServletContextAware; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.w3c.dom.Attr; import org.w3c.dom.Document; import org.w3c.dom.Text; /** * <font size=5><b></b></font> * * @author alistair */ public class AttributeAuthority extends HandlerInterceptorAdapter implements ServletContextAware { /** The ServletContext, passed to us by Spring as we are ServletContextAware */ private ServletContext servletContext = null; /** Our logger */ private static final Logger logger = Logger.getLogger(AttributeAuthority.class.getName()); /** The attributors to use */ private org.guanxi.idp.farm.attributors.Attributor[] attributor = null; /** Our profile specific attribute mapper */ protected AttributeMap mapper = null; /** Our ARP engine */ protected ARPEngine arpEngine = null; /** These SPs get the new format for eduPersonTargetedID */ private ArrayList<String> saml2EduPersonTargetedIDSPEntityIDs = null; public void init() throws ServletException { } public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { // Load up the config file IdpDocument.Idp idpConfig = (IdpDocument.Idp) servletContext.getAttribute(Guanxi.CONTEXT_ATTR_IDP_CONFIG); String nameIdentifier = null; RequestType samlRequest = null; RequestDocument samlRequestDoc = null; try { /* Parse the SOAP message that contains the SAML Request... * XMLBeans 2.2.0 has problems parsing from an InputStream though */ BufferedReader buffer = request.getReader(); StringBuffer stringBuffer = new StringBuffer(); String line = null; while ((line = buffer.readLine()) != null) { stringBuffer.append(line); } buffer.close(); logger.debug( "Parsing attribute query : " + stringBuffer.length() + " bytes " + stringBuffer.toString()); EnvelopeDocument soapEnvelopeDoc = EnvelopeDocument.Factory.parse(stringBuffer.toString()); Body soapBody = soapEnvelopeDoc.getEnvelope().getBody(); // ...and extract the SAML Request samlRequestDoc = RequestDocument.Factory.parse(soapBody.getDomNode().getFirstChild()); samlRequest = samlRequestDoc.getRequest(); } catch (UnsupportedEncodingException uee) { logger.error("Can't parse input text", uee); } catch (IOException ioe) { logger.error("Can't read the input", ioe); } catch (IllegalStateException ise) { logger.error("Can't read input - already read", ise); } catch (XmlException xe) { logger.error("Can't parse SOAP AttributeQuery", xe); } // The first thing we need to do is find out what Principal is being referred to by the requesting SP // Get the SP's providerId from the attribute query String spProviderId = samlRequest.getAttributeQuery().getResource(); // Get the NameIdentifier of the query... nameIdentifier = samlRequest.getAttributeQuery().getSubject().getNameIdentifier().getStringValue(); // ...and retrieve their SP specific details from the session GuanxiPrincipal principal = (GuanxiPrincipal) servletContext.getAttribute(nameIdentifier); HashMap<String, String> namespaces = new HashMap<String, String>(); namespaces.put(Shibboleth.NS_SAML_10_PROTOCOL, Shibboleth.NS_PREFIX_SAML_10_PROTOCOL); namespaces.put(Shibboleth.NS_SAML_10_ASSERTION, Shibboleth.NS_PREFIX_SAML_10_ASSERTION); XmlOptions xmlOptions = new XmlOptions(); xmlOptions.setSavePrettyPrint(); xmlOptions.setSavePrettyPrintIndent(2); xmlOptions.setUseDefaultNamespace(); xmlOptions.setSaveAggressiveNamespaces(); xmlOptions.setSaveSuggestedPrefixes(namespaces); xmlOptions.setSaveNamespacesFirst(); // Build a SAML Response to send to the SP ResponseDocument samlResponseDoc = ResponseDocument.Factory.newInstance(xmlOptions); ResponseType samlResponse = samlResponseDoc.addNewResponse(); samlResponse.setResponseID(Utils.createNCNameID()); samlResponse.setMajorVersion(new BigInteger("1")); samlResponse.setMinorVersion(new BigInteger("1")); samlResponse.setIssueInstant(Calendar.getInstance()); samlResponse.setInResponseTo(samlRequest.getRequestID()); Utils.zuluXmlObject(samlResponse, 0); // Get a SAML Status ready StatusDocument statusDoc = StatusDocument.Factory.newInstance(); StatusType status = statusDoc.addNewStatus(); StatusCodeType topLevelStatusCode = status.addNewStatusCode(); // From now on, any exceptions will be propagated to the SP using <Status> // Is this a locally registered SP? String spID = null; ServiceProvider[] spList = idpConfig.getServiceProviderArray(); for (int c = 0; c < spList.length; c++) { if (spList[c].getName().equals(spProviderId)) { // We trust locally registered SPs spID = spProviderId; } } /* Not a locally registered SP, so full validation rules. * * The client's X509Certificate chain will only be available if the server is configured to ask for it. * In Tomcat's case, this means configuring client authentication: * clientAuth="want" * If you use clientAuth="true" you'll need to put the AA on a different port from the SSO as the SSO * doesn't use certificates as it's accessed by a browser. The AA is only accessed by a machine (SP). */ if (spID == null) { EntityFarm farm = (EntityFarm) servletContext.getAttribute(Guanxi.CONTEXT_ATTR_IDP_ENTITY_FARM); EntityManager manager = farm.getEntityManagerForID(spProviderId); if (manager != null) { if (manager.getMetadata(spProviderId) != null) { if (manager.getTrustEngine() != null) { if (!manager.getTrustEngine().trustEntity(manager.getMetadata(spProviderId), (X509Certificate[]) request .getAttribute("javax.servlet.request.X509Certificate"))) { logger.error("Failed to trust SP '" + spProviderId); topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_ERROR)); samlResponse.setStatus(status); samlResponseDoc.save(response.getOutputStream()); return false; } } else { logger.error("Manager could not find trust engine for SP '" + spProviderId); topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_ERROR)); samlResponse.setStatus(status); samlResponseDoc.save(response.getOutputStream()); return false; } } else { logger.error("Manager could not find metadata for SP '" + spProviderId); topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_ERROR)); samlResponse.setStatus(status); samlResponseDoc.save(response.getOutputStream()); return false; } } else { logger.error("Could not find manager for SP '" + spProviderId); topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_ERROR)); samlResponse.setStatus(status); samlResponseDoc.save(response.getOutputStream()); return false; } } // Did we get the principal from the request? if (principal == null) { // If not, there's nothing we can do about attributes. topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_ERROR)); samlResponse.setStatus(status); samlResponseDoc.save(response.getOutputStream()); return false; } // Get their attributes UserAttributesDocument attributesDoc = UserAttributesDocument.Factory.newInstance(); UserAttributesDocument.UserAttributes attributes = attributesDoc.addNewUserAttributes(); for (org.guanxi.idp.farm.attributors.Attributor attr : attributor) { attr.getAttributes(principal, spProviderId, arpEngine, mapper, attributes); } // Set the Status for the SAML Response topLevelStatusCode.setValue(new QName("", Shibboleth.SAMLP_SUCCESS)); samlResponse.setStatus(status); // Get a new Assertion ready for the AttributeStatement nodes AssertionDocument assertionDoc = AssertionDocument.Factory.newInstance(); AssertionType assertion = assertionDoc.addNewAssertion(); assertion.setAssertionID(Utils.createNCNameID()); assertion.setMajorVersion(new BigInteger("1")); assertion.setMinorVersion(new BigInteger("1")); assertion.setIssuer(principal.getIssuerFor(spProviderId)); assertion.setIssueInstant(Calendar.getInstance()); Utils.zuluXmlObject(assertion, 0); // Conditions for the assertions ConditionsDocument conditionsDoc = ConditionsDocument.Factory.newInstance(); ConditionsType conditions = conditionsDoc.addNewConditions(); conditions.setNotBefore(Calendar.getInstance()); conditions.setNotOnOrAfter(Calendar.getInstance()); Utils.zuluXmlObject(conditions, 5); assertion.setConditions(conditions); // Add the attributes if there are any AttributeStatementDocument attrStatementDoc = addAttributesFromFarm(attributesDoc, samlRequest.getAttributeQuery().getSubject().getNameIdentifier().getNameQualifier(), spProviderId); // If a user has no attributes we shouldn't add an Assertion or Subject if (attrStatementDoc != null) { SubjectType subject = attrStatementDoc.getAttributeStatement().addNewSubject(); NameIdentifierType nameID = subject.addNewNameIdentifier(); nameID.setFormat(samlRequest.getAttributeQuery().getSubject().getNameIdentifier().getFormat()); nameID.setNameQualifier( samlRequest.getAttributeQuery().getSubject().getNameIdentifier().getNameQualifier()); nameID.setStringValue( samlRequest.getAttributeQuery().getSubject().getNameIdentifier().getStringValue()); assertion.setAttributeStatementArray( new AttributeStatementType[] { attrStatementDoc.getAttributeStatement() }); samlResponse.setAssertionArray(new AssertionType[] { assertion }); } // Get the config ready for signing SecUtilsConfig secUtilsConfig = new SecUtilsConfig(); secUtilsConfig.setKeystoreFile(principal.getSigningCredsFor(spProviderId).getKeystoreFile()); secUtilsConfig.setKeystorePass(principal.getSigningCredsFor(spProviderId).getKeystorePassword()); secUtilsConfig.setKeystoreType(principal.getSigningCredsFor(spProviderId).getKeystoreType()); secUtilsConfig.setPrivateKeyAlias(principal.getSigningCredsFor(spProviderId).getPrivateKeyAlias()); secUtilsConfig.setPrivateKeyPass(principal.getSigningCredsFor(spProviderId).getPrivateKeyPassword()); secUtilsConfig.setCertificateAlias(principal.getSigningCredsFor(spProviderId).getCertificateAlias()); secUtilsConfig.setKeyType(principal.getSigningCredsFor(spProviderId).getKeyType()); response.setContentType("text/xml"); // SOAP message to hold the SAML Response EnvelopeDocument soapResponseDoc = EnvelopeDocument.Factory.newInstance(); Envelope soapEnvelope = soapResponseDoc.addNewEnvelope(); Body soapBody = soapEnvelope.addNewBody(); // Do we need to sign the assertion? boolean samlAddedToResponse = false; String resource = request.getParameter(samlRequest.getAttributeQuery().getResource()); if (resource != null) { EntityDescriptorType sp = (EntityDescriptorType) servletContext.getAttribute(resource); if (sp != null) { if (sp.getSPSSODescriptorArray(0) != null) { if (sp.getSPSSODescriptorArray(0).getWantAssertionsSigned()) { // Break out to DOM land to get the SAML Response signed... Document signedDoc = null; try { // Add a signed assertion to the response samlAddedToResponse = true; // Need to use newDomNode to preserve namespace information signedDoc = SecUtils.getInstance().sign(secUtilsConfig, (Document) samlResponseDoc.newDomNode(xmlOptions), ""); // Add the SAML Response to the SOAP message soapBody.getDomNode().appendChild(soapBody.getDomNode().getOwnerDocument() .importNode(signedDoc.getDocumentElement(), true)); } catch (GuanxiException ge) { logger.error(ge); } } // if (sp.getSPSSODescriptorArray(0).getWantAssertionsSigned()) } // if (sp.getSPSSODescriptorArray(0) != null) } } if (!samlAddedToResponse) { // Add the unsigned SAML Response to the SOAP message soapBody.getDomNode().appendChild( soapBody.getDomNode().getOwnerDocument().importNode(samlResponse.newDomNode(xmlOptions), true)); } // Debug syphoning? if (idpConfig.getDebug() != null) { if (idpConfig.getDebug().getSypthonAttributeAssertions() != null) { if (idpConfig.getDebug().getSypthonAttributeAssertions().equals("yes")) { logger.info("======================================================="); logger.info("Response to AttributeQuery by " + spProviderId); logger.info(""); StringWriter sw = new StringWriter(); soapResponseDoc.save(sw, xmlOptions); logger.info(sw.toString()); logger.info(""); logger.info("======================================================="); } } } soapResponseDoc.save(response.getOutputStream(), xmlOptions); return false; } private AttributeStatementDocument addAttributesFromFarm(UserAttributesDocument guanxiAttrFarmOutput, String nameQualifier, String spProviderId) { AttributeStatementDocument attrStatementDoc = AttributeStatementDocument.Factory.newInstance(); AttributeStatementType attrStatement = attrStatementDoc.addNewAttributeStatement(); boolean hasAttrs = false; for (int c = 0; c < guanxiAttrFarmOutput.getUserAttributes().getAttributeArray().length; c++) { hasAttrs = true; 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].getAttributeName().equals(attributorAttr.getName())) { attribute = existingAttrs[cc]; } } } // New attribute, not yet processed if (attribute == null) { attribute = attrStatement.addNewAttribute(); attribute.setAttributeName(attributorAttr.getName()); attribute.setAttributeNamespace(Shibboleth.NS_ATTRIBUTES); } XmlObject attrValue = attribute.addNewAttributeValue(); // Deal with scoped eduPerson attributes if ((attribute.getAttributeName().equals(EduPerson.EDUPERSON_SCOPED_AFFILIATION)) || (attribute.getAttributeName().equals(EduPerson.EDUPERSON_TARGETED_ID))) { // Check if the scope is present... if (!attributorAttr.getValue().contains(EduPerson.EDUPERSON_SCOPED_DELIMITER)) { // ...if not, add the error scope logger.error(attribute.getAttributeName() + " has no scope, adding " + EduPerson.EDUPERSON_NO_SCOPE_DEFINED); attributorAttr.setValue(attributorAttr.getValue() + EduPerson.EDUPERSON_SCOPED_DELIMITER + EduPerson.EDUPERSON_NO_SCOPE_DEFINED); } String[] parts = attributorAttr.getValue().split(EduPerson.EDUPERSON_SCOPED_DELIMITER); Attr scopeAttribute = attrValue.getDomNode().getOwnerDocument() .createAttribute(EduPerson.EDUPERSON_SCOPE_ATTRIBUTE); scopeAttribute.setValue(parts[1]); attrValue.getDomNode().getAttributes().setNamedItem(scopeAttribute); Text valueNode = attrValue.getDomNode().getOwnerDocument().createTextNode(parts[0]); attrValue.getDomNode().appendChild(valueNode); } else { Text valueNode = attrValue.getDomNode().getOwnerDocument() .createTextNode(attributorAttr.getValue()); attrValue.getDomNode().appendChild(valueNode); } // Release the newer eduPersonTargetedID format to specific SPs // internet2-mace-dir-saml-attributes-200804a : 2.3.2.1.1 Recommended Name and Syntax if (attribute.getAttributeName().equals(EduPerson.EDUPERSON_TARGETED_ID)) { if (getsNewEPTID(spProviderId)) { NameIDDocument nameIDDoc = NameIDDocument.Factory.newInstance(); NameIDType nameID = nameIDDoc.addNewNameID(); nameID.setFormat(SAML.SAML2_ATTRIBUTE_FORMAT_NAMEID_PERSISTENT); nameID.setNameQualifier(nameQualifier); nameID.setSPNameQualifier(spProviderId); if (attributorAttr.getValue().contains("@")) { attributorAttr.setValue(attributorAttr.getValue().split("@")[0]); } nameID.setStringValue(attributorAttr.getValue()); AttributeType saml2Attribute = attrStatement.addNewAttribute(); saml2Attribute.setAttributeName( EduPersonOID.ATTRIBUTE_NAME_PREFIX + EduPersonOID.OID_EDUPERSON_TARGETED_ID); saml2Attribute.setAttributeNamespace(Shibboleth.NS_ATTRIBUTES); XmlObject saml2AttrValue = saml2Attribute.addNewAttributeValue(); saml2AttrValue.getDomNode().appendChild( saml2AttrValue.getDomNode().getOwnerDocument().importNode(nameID.getDomNode(), true)); // Don't release the legacy format of eduPersonTargetedID int count = 0; AttributeType[] existingAttributes = attrStatement.getAttributeArray(); for (AttributeType existingAttribute : existingAttributes) { if (existingAttribute.getAttributeName().equals(EduPerson.EDUPERSON_TARGETED_ID)) { attrStatement.removeAttribute(count); } count++; } } } } if (hasAttrs) return attrStatementDoc; else return null; } private boolean getsNewEPTID(String spEntityID) { if (saml2EduPersonTargetedIDSPEntityIDs == null) { return false; } for (String entityID : saml2EduPersonTargetedIDSPEntityIDs) { if (entityID.equals(spEntityID)) { return true; } } return false; } // Getters public ARPEngine getArpEngine() { return arpEngine; } public AttributeMap getMapper() { return mapper; } // Setters public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } public void setAttributor(org.guanxi.idp.farm.attributors.Attributor[] attributor) { this.attributor = attributor; } public void setMapper(AttributeMap mapper) { this.mapper = mapper; } public void setArpEngine(ARPEngine arpEngine) { this.arpEngine = arpEngine; } public void setSaml2EduPersonTargetedIDSPEntityIDs(ArrayList<String> saml2EduPersonTargetedIDSPEntityIDs) { this.saml2EduPersonTargetedIDSPEntityIDs = saml2EduPersonTargetedIDSPEntityIDs; } }