/* * Copyright (c) 2014, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * * WSO2 Inc. licenses this file to you under the Apache License, * Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * * */ package org.wso2.carbon.identity.entitlement.proxy.wsxacml; import org.apache.axiom.om.OMAbstractFactory; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMNamespace; import org.apache.axiom.om.util.AXIOMUtil; import org.apache.axis2.addressing.EndpointReference; import org.apache.axis2.client.Options; import org.apache.axis2.client.ServiceClient; import org.apache.axis2.transport.http.HttpTransportProperties; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.xerces.impl.Constants; import org.apache.xerces.util.SecurityManager; import org.apache.xml.security.Init; import org.apache.xml.security.c14n.Canonicalizer; import org.apache.xml.security.signature.XMLSignature; import org.joda.time.DateTime; import org.opensaml.Configuration; import org.opensaml.DefaultBootstrap; import org.opensaml.saml2.core.Assertion; import org.opensaml.saml2.core.Issuer; import org.opensaml.saml2.core.Response; import org.opensaml.saml2.core.impl.IssuerBuilder; import org.opensaml.xacml.ctx.RequestType; import org.opensaml.xacml.ctx.ResponseType; import org.opensaml.xacml.profile.saml.XACMLAuthzDecisionQueryType; import org.opensaml.xacml.profile.saml.XACMLAuthzDecisionStatementType; import org.opensaml.xacml.profile.saml.impl.XACMLAuthzDecisionQueryTypeImplBuilder; import org.opensaml.xml.ConfigurationException; import org.opensaml.xml.XMLObject; import org.opensaml.xml.XMLObjectBuilder; import org.opensaml.xml.io.Marshaller; import org.opensaml.xml.io.MarshallerFactory; import org.opensaml.xml.io.Unmarshaller; import org.opensaml.xml.io.UnmarshallerFactory; import org.opensaml.xml.security.x509.BasicX509Credential; import org.opensaml.xml.security.x509.X509Credential; import org.opensaml.xml.signature.KeyInfo; import org.opensaml.xml.signature.Signature; import org.opensaml.xml.signature.SignatureValidator; import org.opensaml.xml.signature.Signer; import org.opensaml.xml.signature.X509Certificate; import org.opensaml.xml.signature.X509Data; import org.opensaml.xml.validation.ValidationException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.bootstrap.DOMImplementationRegistry; import org.w3c.dom.ls.DOMImplementationLS; import org.w3c.dom.ls.LSOutput; import org.w3c.dom.ls.LSSerializer; import org.wso2.carbon.base.ServerConfiguration; import org.wso2.carbon.identity.entitlement.proxy.AbstractEntitlementServiceClient; import org.wso2.carbon.identity.entitlement.proxy.Attribute; import org.wso2.carbon.identity.entitlement.proxy.XACMLRequetBuilder; import org.wso2.carbon.identity.entitlement.proxy.exception.EntitlementProxyException; import org.wso2.carbon.identity.entitlement.proxy.util.CarbonEntityResolver; import javax.xml.XMLConstants; import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.charset.Charset; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public class WSXACMLEntitlementServiceClient extends AbstractEntitlementServiceClient { private static final Log log = LogFactory.getLog(WSXACMLEntitlementServiceClient.class); private static final String SECURITY_MANAGER_PROPERTY = Constants.XERCES_PROPERTY_PREFIX + Constants.SECURITY_MANAGER_PROPERTY; private static final int ENTITY_EXPANSION_LIMIT = 0; public static final String ISSUER_URL = "https://identity.carbon.wso2.org"; public static final String DOCUMENT_BUILDER_FACTORY = "javax.xml.parsers.DocumentBuilderFactory"; public static final String DOCUMENT_BUILDER_FACTORY_IMPL = "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl"; private static boolean isBootStrapped = false; public static final String URN_OASIS_NAMES_TC_XACML_2_0_CONTEXT_SCHEMA_OS = "urn:oasis:names:tc:xacml:2.0:context:schema:os"; private static OMNamespace xacmlContextNS = OMAbstractFactory.getOMFactory().createOMNamespace (URN_OASIS_NAMES_TC_XACML_2_0_CONTEXT_SCHEMA_OS, "xacml-context"); HttpTransportProperties.Authenticator authenticator; private String serverUrl; public WSXACMLEntitlementServiceClient(String serverUrl, String userName, String password) { this.serverUrl = serverUrl; authenticator = new HttpTransportProperties.Authenticator(); authenticator.setUsername(userName); authenticator.setPassword(password); authenticator.setPreemptiveAuthentication(true); } /** * Bootstrap the OpenSAML2 library only if it is not bootstrapped. */ public static void doBootstrap() { if (!isBootStrapped) { try { DefaultBootstrap.bootstrap(); isBootStrapped = true; } catch (ConfigurationException e) { log.error("Error in bootstrapping the OpenSAML2 library", e); } } } /** * Set relevant xacml namespace to all the children in the given iterator. * * @param iterator: Iterator for all children inside OMElement */ private static void setXACMLNamespace(Iterator iterator) { while (iterator.hasNext()) { OMElement omElemnt2 = (OMElement) iterator.next(); omElemnt2.setNamespace(xacmlContextNS); if (omElemnt2.getChildElements().hasNext()) { setXACMLNamespace(omElemnt2.getChildElements()); } } } /** * Create the issuer object to be added * * @return : the issuer of the statements */ private static Issuer createIssuer() { IssuerBuilder issuer = (IssuerBuilder) org.opensaml.xml.Configuration.getBuilderFactory(). getBuilder(Issuer.DEFAULT_ELEMENT_NAME); Issuer issuerObject = issuer.buildObject(); issuerObject.setValue(ISSUER_URL); issuerObject.setSPProvidedID("SPPProvierId"); return issuerObject; } /** * Get decision in a secured manner using the * SAML implementation of XACML using X.509 credentials * * @return decision extracted from the SAMLResponse sent from PDP * @throws Exception */ @Override public String getDecision(Attribute[] attributes, String appId) throws Exception { String xacmlRequest; String xacmlAuthzDecisionQuery; OMElement samlResponseElement; String samlResponse; String result; try { xacmlRequest = XACMLRequetBuilder.buildXACML3Request(attributes); xacmlAuthzDecisionQuery = buildSAMLXACMLAuthzDecisionQuery(xacmlRequest); ServiceClient sc = new ServiceClient(); Options opts = new Options(); opts.setTo(new EndpointReference(serverUrl + "ws-xacml")); opts.setAction("XACMLAuthzDecisionQuery"); opts.setProperty(org.apache.axis2.transport.http.HTTPConstants.AUTHENTICATE, authenticator); opts.setManageSession(true); sc.setOptions(opts); samlResponseElement = sc.sendReceive(AXIOMUtil.stringToOM(xacmlAuthzDecisionQuery)); samlResponse = samlResponseElement.toString(); result = extractXACMLResponse(samlResponse); sc.cleanupTransport(); return result; } catch (Exception e) { log.error("Error occurred while getting decision using SAML.", e); throw new Exception("Error occurred while getting decision using SAML.", e); } } @Override public boolean subjectCanActOnResource(String subjectType, String alias, String actionId, String resourceId, String domainId, String appId) throws Exception { return false; } @Override public boolean subjectCanActOnResource(String subjectType, String alias, String actionId, String resourceId, Attribute[] attributes, String domainId, String appId) throws Exception { return false; } @Override public List<String> getResourcesForAlias(String alias, String appId) throws Exception { return new ArrayList<>(); } @Override public List<String> getActionableResourcesForAlias(String alias, String appId) throws Exception { return new ArrayList<>(); } @Override public List<String> getActionableChildResourcesForAlias(String alias, String parentResource, String action, String appId) throws Exception { return new ArrayList<>(); } @Override public List<String> getActionsForResource(String alias, String resources, String appId) throws Exception { return new ArrayList<>(); } /** * Extract XACML response from the SAML response * * @param samlResponse : SAML response that carries the XACML response from PDP * @return the XACML response */ private String extractXACMLResponse(String samlResponse) throws EntitlementProxyException { Response samlResponseObject = null; ResponseType xacmlResponse = null; doBootstrap(); Init.init(); try { samlResponseObject = (Response) unmarshall(samlResponse); } catch (Exception e) { log.error("Error occurred while unmarshalling the SAML Response!", e); throw new EntitlementProxyException("Error occurred while unmarshalling the SAML Response!", e); } String xacmlResponseString = null; //Access the XACML response only if Issuer and the Signature are valid. if (validateIssuer(samlResponseObject.getIssuer())) { if (validateSignature(samlResponseObject.getSignature())) { List<Assertion> assertionList = samlResponseObject.getAssertions(); //under the assumption that the first assertion carries the decisionStatement Assertion assertion1 = assertionList.get(0); if (validateIssuer(assertion1.getIssuer())) { xacmlResponse = ((XACMLAuthzDecisionStatementType) assertion1. getStatements(XACMLAuthzDecisionStatementType.TYPE_NAME_XACML20).get(0)).getResponse(); try { xacmlResponseString = org.apache.axis2.util.XMLUtils.toOM(xacmlResponse.getDOM()). toString().replaceAll("xacml-context:", ""); } catch (Exception e) { log.error("Error occurred while converting the SAML Response DOM to OMElement", e); throw new EntitlementProxyException( "Error occurred while converting the SAML Response DOM to OMElement", e); } } else { log.debug("The submitted issuer is not valid for assertion."); } } else { log.debug("The submitted signature is not valid for the saml response."); } } else { log.debug("The submitted issuer is not valid for the saml response."); } return xacmlResponseString; } /** * Check for the validity of the issuer * * @param issuer :who makes the claims inside the Query * @return whether the issuer is valid */ private boolean validateIssuer(Issuer issuer) { boolean isValidated = false; if (ISSUER_URL.equals(issuer.getValue()) && "SPPProvider".equals(issuer.getSPProvidedID())) { isValidated = true; } return isValidated; } /** * Check the validity of the Signature * * @param signature : XML Signature that authenticates the assertion * @return whether the signature is valid * @throws Exception */ private boolean validateSignature(Signature signature) throws EntitlementProxyException { boolean isSignatureValid = false; try { SignatureValidator validator = new SignatureValidator(getPublicX509CredentialImpl()); validator.validate(signature); isSignatureValid = true; } catch (ValidationException e) { log.warn("Signature validation failed.", e); } return isSignatureValid; } /** * get public X509Credentials using the configured basic credentials * * @return X509Credential implementation */ private X509CredentialImpl getPublicX509CredentialImpl() throws EntitlementProxyException { X509CredentialImpl credentialImpl = null; // load the default public cert using the configuration in carbon.xml java.security.cert.X509Certificate cert = createBasicCredentials().getEntityCertificate(); credentialImpl = new X509CredentialImpl(cert); return credentialImpl; } /** * Build the SAML XACMLAuthzDecisionQuery to be passed to PDP * * @param xacmlRequest:XACML request with subject, action, resource and environment * @return The XACMLAuthzDecisionQuery */ private String buildSAMLXACMLAuthzDecisionQuery(String xacmlRequest) throws EntitlementProxyException { RequestType request = null; doBootstrap(); String xacmlAuthzDecisionQueryString = null; try { request = ((RequestType) unmarshall(formatRequest(xacmlRequest))); } catch (Exception e) { log.error("Error occurred while unmarshalling the XACML Request!", e); throw new EntitlementProxyException("Error occurred while unmarshalling the XACML Request!", e); } XACMLAuthzDecisionQueryTypeImplBuilder xacmlauthz = (XACMLAuthzDecisionQueryTypeImplBuilder) org.opensaml.xml.Configuration.getBuilderFactory(). getBuilder(XACMLAuthzDecisionQueryType.TYPE_NAME_XACML20); XACMLAuthzDecisionQueryType xacmlAuthzDecisionQuery = xacmlauthz .buildObject(XACMLAuthzDecisionQueryType.TYPE_NAME_XACML20); DateTime currentTime = new DateTime(); xacmlAuthzDecisionQuery.setRequest(request); xacmlAuthzDecisionQuery.setInputContextOnly(true); xacmlAuthzDecisionQuery.setReturnContext(false); xacmlAuthzDecisionQuery.setIssueInstant(currentTime); xacmlAuthzDecisionQuery.setIssuer(createIssuer()); try { xacmlAuthzDecisionQuery = setSignature(xacmlAuthzDecisionQuery, XMLSignature.ALGO_ID_SIGNATURE_RSA, createBasicCredentials()); } catch (Exception e) { log.error("Error while building SAMLXACMLAuthzDecisionQuery from the given xacml request", e); throw new EntitlementProxyException( "Error while building SAMLXACMLAuthzDecisionQuery from the given xacml request", e); } if (xacmlAuthzDecisionQuery != null) { try { xacmlAuthzDecisionQueryString = marshall(xacmlAuthzDecisionQuery); xacmlAuthzDecisionQueryString = xacmlAuthzDecisionQueryString.replace ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>", "").replace("\n", ""); } catch (Exception e) { log.error("Error occurred while marshalling XACMLAuthzDecisionQuery.", e); throw new EntitlementProxyException("Error occurred while marshalling XACMLAuthzDecisionQuery.", e); } } return xacmlAuthzDecisionQueryString; } /** * Format the sent in request as required by OpenSAML * * @param xacmlRequest : received XACML request * @return formatted request * @throws Exception */ private String formatRequest(String xacmlRequest) throws EntitlementProxyException { xacmlRequest = xacmlRequest.replace("\n", ""); OMElement omElemnt = null; try { omElemnt = org.apache.axiom.om.util.AXIOMUtil.stringToOM(xacmlRequest); omElemnt.setNamespace(xacmlContextNS); Iterator childIterator = omElemnt.getChildElements(); setXACMLNamespace(childIterator); return omElemnt.toString(); } catch (Exception e) { log.error("Error occurred while formatting the XACML request", e); throw new EntitlementProxyException("Error occurred while formatting the XACML request", e); } } /** * Constructing the SAML or XACML Objects from a String * * @param xmlString Decoded SAML or XACML String * @return SAML or XACML Object * @throws EntitlementProxyException */ private XMLObject unmarshall(String xmlString) throws EntitlementProxyException { try { doBootstrap(); DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilderFactory.setExpandEntityReferences(false); documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); SecurityManager securityManager = new SecurityManager(); securityManager.setEntityExpansionLimit(ENTITY_EXPANSION_LIMIT); documentBuilderFactory.setAttribute(SECURITY_MANAGER_PROPERTY, securityManager); DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder(); docBuilder.setEntityResolver(new CarbonEntityResolver()); Document document = docBuilder.parse(new ByteArrayInputStream(xmlString.trim().getBytes(Charset.forName ("UTF-8")))); Element element = document.getDocumentElement(); UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory(); Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element); return unmarshaller.unmarshall(element); } catch (Exception e) { log.error("Error in constructing XML(SAML or XACML) Object from the encoded String", e); throw new EntitlementProxyException( "Error in constructing XML(SAML or XACML) from the encoded String", e); } } /** * Serialize XML objects * * @param xmlObject : XACML or SAML objects to be serialized * @return serialized XACML or SAML objects */ private String marshall(XMLObject xmlObject) throws EntitlementProxyException { try { doBootstrap(); System.setProperty( DOCUMENT_BUILDER_FACTORY, DOCUMENT_BUILDER_FACTORY_IMPL); MarshallerFactory marshallerFactory = org.opensaml.xml.Configuration.getMarshallerFactory(); Marshaller marshaller = marshallerFactory.getMarshaller(xmlObject); Element element = marshaller.marshall(xmlObject); ByteArrayOutputStream byteArrayOutputStrm = new ByteArrayOutputStream(); DOMImplementationRegistry registry = DOMImplementationRegistry.newInstance(); DOMImplementationLS impl = (DOMImplementationLS) registry.getDOMImplementation("LS"); LSSerializer writer = impl.createLSSerializer(); LSOutput output = impl.createLSOutput(); output.setByteStream(byteArrayOutputStrm); writer.write(element, output); return new String(byteArrayOutputStrm.toByteArray(), Charset.forName("UTF-8")); } catch (Exception e) { log.error("Error Serializing the SAML Response"); throw new EntitlementProxyException("Error Serializing the SAML Response", e); } } /** * Overloaded method to sign a XACMLAuthzDecisionQuery * * @param xacmlAuthzDecisionQueryType : xacmlAuthzdecisonQuery to be signed * @param signatureAlgorithm : algorithm to be used in signing * @param cred : signing credentials * @return signed xacmlAuthzDecisionQuery * @throws EntitlementProxyException */ private XACMLAuthzDecisionQueryType setSignature( XACMLAuthzDecisionQueryType xacmlAuthzDecisionQueryType, String signatureAlgorithm, X509Credential cred) throws EntitlementProxyException { doBootstrap(); try { Signature signature = (Signature) buildXMLObject(Signature.DEFAULT_ELEMENT_NAME); signature.setSigningCredential(cred); signature.setSignatureAlgorithm(signatureAlgorithm); signature.setCanonicalizationAlgorithm(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS); try { KeyInfo keyInfo = (KeyInfo) buildXMLObject(KeyInfo.DEFAULT_ELEMENT_NAME); X509Data data = (X509Data) buildXMLObject(X509Data.DEFAULT_ELEMENT_NAME); X509Certificate cert = (X509Certificate) buildXMLObject(X509Certificate.DEFAULT_ELEMENT_NAME); String value = org.apache.xml.security.utils.Base64.encode(cred.getEntityCertificate().getEncoded()); cert.setValue(value); data.getX509Certificates().add(cert); keyInfo.getX509Datas().add(data); signature.setKeyInfo(keyInfo); } catch (CertificateEncodingException e) { if (log.isDebugEnabled()) { log.debug("Certificate Encoding Exception occurred : ", e); } throw new EntitlementProxyException("Error getting the certificate."); } xacmlAuthzDecisionQueryType.setSignature(signature); List<Signature> signatureList = new ArrayList<Signature>(); signatureList.add(signature); //Marshall and Sign MarshallerFactory marshallerFactory = org.opensaml.xml.Configuration.getMarshallerFactory(); Marshaller marshaller = marshallerFactory.getMarshaller(xacmlAuthzDecisionQueryType); marshaller.marshall(xacmlAuthzDecisionQueryType); org.apache.xml.security.Init.init(); Signer.signObjects(signatureList); return xacmlAuthzDecisionQueryType; } catch (Exception e) { throw new EntitlementProxyException("Error When signing the assertion.", e); } } /** * Create basic X509 credentials using server configuration * * @return basicX509Credential */ private BasicX509Credential createBasicCredentials() { PrivateKey issuerPK = null; Certificate certificate = null; ServerConfiguration serverConfig = ServerConfiguration.getInstance(); String ksPassword = serverConfig.getFirstProperty("Security.KeyStore.Password"); String ksLocation = serverConfig.getFirstProperty("Security.KeyStore.Location"); String keyAlias = serverConfig.getFirstProperty("Security.KeyStore.KeyAlias"); String ksType = serverConfig.getFirstProperty("Security.KeyStore.Type"); String privateKeyPassword = serverConfig.getFirstProperty("Security.KeyStore.KeyPassword"); try { FileInputStream fis = new FileInputStream(ksLocation); BufferedInputStream bis = new BufferedInputStream(fis); KeyStore keyStore = KeyStore.getInstance(ksType); keyStore.load(bis, ksPassword.toCharArray()); bis.close(); issuerPK = (PrivateKey) keyStore.getKey(keyAlias, privateKeyPassword.toCharArray()); certificate = keyStore.getCertificate(keyAlias); } catch (KeyStoreException e) { log.error("Error in getting a keystore.", e); } catch (FileNotFoundException e) { log.error("Error in reading the keystore file from given the location.", e); } catch (CertificateException e) { log.error("Error in creating a X.509 certificate.", e); } catch (NoSuchAlgorithmException e) { log.error("Error in loading the keystore.", e); } catch (IOException e) { log.error("Error in reading keystore file.", e); } catch (UnrecoverableKeyException e) { log.error("Error in getting the private key.", e); } BasicX509Credential basicCredential = new BasicX509Credential(); basicCredential.setEntityCertificate((java.security.cert.X509Certificate) certificate); basicCredential.setPrivateKey(issuerPK); return basicCredential; } /** * Create XMLObject from a given QName * * @param objectQName: QName of the object to be built into a XMLObject * @return built xmlObject * @throws EntitlementProxyException */ private XMLObject buildXMLObject(QName objectQName) throws EntitlementProxyException { XMLObjectBuilder builder = org.opensaml.xml.Configuration.getBuilderFactory().getBuilder(objectQName); if (builder == null) { throw new EntitlementProxyException("Unable to retrieve builder for object QName " + objectQName); } return builder.buildObject(objectQName.getNamespaceURI(), objectQName.getLocalPart(), objectQName.getPrefix()); } }