/* * 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.security.pox; import org.apache.axiom.om.OMElement; import org.apache.axiom.om.OMNode; import org.apache.axiom.om.util.Base64; import org.apache.axiom.soap.SOAPEnvelope; import org.apache.axiom.soap.SOAPFactory; import org.apache.axiom.soap.SOAPFault; import org.apache.axiom.soap.SOAPHeader; import org.apache.axiom.soap.SOAPHeaderBlock; import org.apache.axis2.AxisFault; import org.apache.axis2.context.MessageContext; import org.apache.axis2.description.AxisService; import org.apache.axis2.description.HandlerDescription; import org.apache.axis2.description.Parameter; import org.apache.axis2.engine.Handler; import org.apache.axis2.transport.http.HTTPConstants; import org.apache.axis2.util.JavaUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.rampart.util.Axis2Util; import org.apache.ws.security.WSConstants; import org.apache.ws.security.WSSecurityException; import org.apache.ws.security.message.WSSecHeader; import org.apache.ws.security.message.WSSecTimestamp; import org.apache.ws.security.message.WSSecUsernameToken; import org.w3c.dom.Document; import org.wso2.carbon.base.ServerConfiguration; import org.wso2.carbon.security.SecurityConfigException; import org.wso2.carbon.security.SecurityConstants; import org.wso2.carbon.security.config.SecurityConfigAdmin; import org.wso2.carbon.security.config.service.SecurityScenarioData; import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.Caching; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * Handler to convert the HTTP basic auth information into * <code>wsse:UsernameToken</code> */ public class POXSecurityHandler implements Handler { public static final String POX_CACHE_MANAGER = "POX_CACHE_MANAGER"; public static final String POX_ENABLED = "pox-security"; private static Log log = LogFactory.getLog(POXSecurityHandler.class); private static String POX_SECURITY_MODULE = "POXSecurityModule"; private HandlerDescription description; @Override /** * @see org.apache.axis2.engine.Handler#cleanup() */ public void cleanup() { // Do Nothing } @Override /** * @see org.apache.axis2.engine.Handler#init(org.apache.axis2.description.HandlerDescription) */ public void init(HandlerDescription description) { this.description = description; } @Override /** * @see org.apache.axis2.engine.Handler#invoke(org.apache.axis2.context.MessageContext) */ public InvocationResponse invoke(MessageContext msgCtx) throws AxisFault { if (msgCtx != null && !msgCtx.isEngaged(POX_SECURITY_MODULE)) { return InvocationResponse.CONTINUE; } AxisService service = msgCtx.getAxisService(); if (service == null) { if (log.isDebugEnabled()) { log.debug("Service not dispatched"); } return InvocationResponse.CONTINUE; } // We do not add details of admin services to the registry, hence if a rest call comes to a // admin service that does not require authentication we simply skip it String isAdminService = (String) service.getParameterValue("adminService"); if (isAdminService != null && JavaUtils.isTrueExplicitly(isAdminService)) { return InvocationResponse.CONTINUE; } String isHiddenService = (String) service.getParameterValue("hiddenService"); if (isHiddenService != null && JavaUtils.isTrueExplicitly(isHiddenService)) { return InvocationResponse.CONTINUE; } String isReverseProxy = System.getProperty("reverseProxyMode"); if (isReverseProxy != null && JavaUtils.isTrueExplicitly(isReverseProxy)) { return InvocationResponse.CONTINUE; } String isPox = null; Cache<String, String> cache = this.getPOXCache(); if (cache != null && cache.get(service.getName()) != null) { isPox = cache.get(service.getName()); } if (isPox != null && JavaUtils.isFalseExplicitly(isPox)) { return InvocationResponse.CONTINUE; } if (msgCtx.isFault() && new Integer(MessageContext.OUT_FAULT_FLOW).equals(msgCtx.getFLOW())) { // we only need to execute this block in Unauthorized situations when basicAuth used // otherwise it should continue the message flow by throwing the incoming fault message since // this is already a fault response - ESBJAVA-2731 if(log.isDebugEnabled()){ log.debug("SOAP Fault occurred and message flow equals to out fault flow. SOAP fault :" + msgCtx .getEnvelope().toString()); } try { String scenarioID = getScenarioId(msgCtx, service); if (scenarioID != null && scenarioID.equals(SecurityConstants.USERNAME_TOKEN_SCENARIO_ID)) { setAuthHeaders(msgCtx); //If request is a REST then remove the soap fault tag contents to avoid it getting in client end if (msgCtx.isDoingREST()) { SOAPFault soapFault = msgCtx.getEnvelope().getBody().getFault(); if (soapFault != null) { Iterator itr = soapFault.getChildren(); while (itr.hasNext()) { OMNode omNode = (OMNode) itr.next(); if (omNode != null) { itr.remove(); } } } } return InvocationResponse.CONTINUE; } } catch (Exception e) { // throwing the same fault which returned by the messageCtx throw new AxisFault("System error", msgCtx.getFailureReason()); } return InvocationResponse.CONTINUE; } if (msgCtx == null || msgCtx.getIncomingTransportName() == null) { return InvocationResponse.CONTINUE; } //return if transport is not https or http (UT scenario should work over http transport as well.) if (!StringUtils.equals("https", msgCtx.getIncomingTransportName()) && !StringUtils.equals("http", msgCtx .getIncomingTransportName())) { return InvocationResponse.CONTINUE; } String basicAuthHeader = getBasicAuthHeaders(msgCtx); boolean soapWithoutSecHeader = isSOAPWithoutSecHeader(msgCtx); /** * Return if incoming message is soap and not associate with soap security headers with the absence of * basic auth headers. This will make sure soap message without security headers giving soap fault * instead unauthorized header */ if (!msgCtx.isDoingREST() && soapWithoutSecHeader && basicAuthHeader == null) { return InvocationResponse.CONTINUE; } //return if incoming message is soap and has soap security headers if (!soapWithoutSecHeader) { return InvocationResponse.CONTINUE; } if (log.isDebugEnabled()) { log.debug("Admin service check failed OR cache miss"); } try { String scenarioID = getScenarioId(msgCtx, service); if (scenarioID != null && scenarioID.equals(SecurityConstants.USERNAME_TOKEN_SCENARIO_ID)) { if (log.isDebugEnabled()) { log.debug("Processing POX security"); } } else { if (cache != null) { cache.put(service.getName(), "false"); } return InvocationResponse.CONTINUE; } String username = null; String password = null; if (basicAuthHeader != null && basicAuthHeader.startsWith("Basic ")) { basicAuthHeader = new String(Base64.decode(basicAuthHeader.substring(6))); int i = basicAuthHeader.indexOf(':'); if (i == -1) { username = basicAuthHeader; } else { username = basicAuthHeader.substring(0, i); } if (i != -1) { password = basicAuthHeader.substring(i + 1); if (StringUtils.equals("", password)) { password = null; } } } if (username == null || password == null || password.trim().length() == 0 || username.trim().length() == 0) { setAuthHeaders(msgCtx); return InvocationResponse.ABORT; } //If no soap header found in the request create new soap header Document doc = null; SOAPEnvelope soapEnvelop = msgCtx.getEnvelope(); if (msgCtx.getEnvelope().getHeader() == null) { SOAPFactory omFac = (SOAPFactory) soapEnvelop.getOMFactory(); SOAPEnvelope newEnvelop = omFac.getDefaultEnvelope(); Iterator itr = soapEnvelop.getBody().getChildren(); while (itr.hasNext()) { OMNode omNode = (OMNode) itr.next(); if (omNode != null) { itr.remove(); newEnvelop.getBody().addChild(omNode); } } doc = Axis2Util.getDocumentFromSOAPEnvelope(newEnvelop, true); } else { doc = Axis2Util.getDocumentFromSOAPEnvelope(soapEnvelop, true); } WSSecHeader secHeader = new WSSecHeader(); secHeader.insertSecurityHeader(doc); WSSecUsernameToken utBuilder = new WSSecUsernameToken(); utBuilder.setPasswordType(WSConstants.PASSWORD_TEXT); utBuilder.setUserInfo(username, password); utBuilder.build(doc, secHeader); WSSecTimestamp tsBuilder = new WSSecTimestamp(); tsBuilder.build(doc, secHeader); /** * Set the new SOAPEnvelope */ msgCtx.setEnvelope(Axis2Util.getSOAPEnvelopeFromDOMDocument(doc, false)); } catch (AxisFault e) { throw e; } catch (WSSecurityException wssEx) { throw new AxisFault("WSDoAllReceiver: Error in converting to Document", wssEx); } catch (Exception e) { throw new AxisFault("System error", e); } return InvocationResponse.CONTINUE; } private void setAuthHeaders(MessageContext msgCtx) throws IOException { String serverName = ServerConfiguration.getInstance().getFirstProperty("Name"); if (serverName == null || serverName.trim().length() == 0) { serverName = "WSO2 Carbon"; } HttpServletResponse response = (HttpServletResponse) msgCtx.getProperty(HTTPConstants.MC_HTTP_SERVLETRESPONSE); // TODO : verify this fix. This is to handle soap fault from UT scenario if(msgCtx.isFault() && response == null){ MessageContext originalContext = (MessageContext) msgCtx.getProperty(MessageContext.IN_MESSAGE_CONTEXT); if(originalContext != null){ response = (HttpServletResponse) originalContext.getProperty(HTTPConstants.MC_HTTP_SERVLETRESPONSE); } } if (response != null) { response.setContentLength(0); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.addHeader("WWW-Authenticate", "BASIC realm=\"" + serverName + "\""); response.flushBuffer(); } else { // if not servlet transport assume it to be nhttp transport msgCtx.setProperty("NIO-ACK-Requested", "true"); msgCtx.setProperty("HTTP_SC", HttpServletResponse.SC_UNAUTHORIZED); Map<String, String> responseHeaders = new HashMap<>(); responseHeaders.put("WWW-Authenticate", "BASIC realm=\"" + serverName + "\""); msgCtx.setProperty(MessageContext.TRANSPORT_HEADERS, responseHeaders); } } private String getScenarioId(MessageContext msgCtx, AxisService service) throws SecurityConfigException { String scenarioID = null; try { scenarioID = (String) service.getParameter(SecurityConstants.SCENARIO_ID_PARAM_NAME).getValue(); } catch (Exception e) { }//ignore if (scenarioID == null) { synchronized (this) { SecurityConfigAdmin securityAdmin = new SecurityConfigAdmin(msgCtx. getConfigurationContext().getAxisConfiguration()); SecurityScenarioData data = securityAdmin.getCurrentScenario(service.getName()); if (data != null) { scenarioID = data.getScenarioId(); try { Parameter param = new Parameter(); param.setName(SecurityConstants.SCENARIO_ID_PARAM_NAME); param.setValue(scenarioID); service.addParameter(param); } catch (AxisFault axisFault) { log.error("Error while adding Scenario ID parameter", axisFault); } } } } return scenarioID; } /** * @param msgCtx message going through the handler chain * @return true if its a soap message without a security header */ private boolean isSOAPWithoutSecHeader(MessageContext msgCtx) { //see whether security header present: if so return false SOAPHeader soapHeader = msgCtx.getEnvelope().getHeader(); if (soapHeader == null) { return true; // no security header } //getting the set of secuirty headers List headerBlocks = soapHeader.getHeaderBlocksWithNSURI(WSConstants.WSSE_NS); // Issue is axiom - a returned collection must not be null if (headerBlocks != null) { Iterator headerBlocksIterator = headerBlocks.iterator(); while (headerBlocksIterator.hasNext()) { Object o=headerBlocksIterator.next(); SOAPHeaderBlock elem = null; OMElement element = null; if (o instanceof SOAPHeaderBlock) { try { elem = (SOAPHeaderBlock)o; } catch (Exception e) { log.error("Error while casting to soap header block", e); } } else { element = ((OMElement) o).cloneOMElement(); } if (elem != null && WSConstants.WSSE_LN.equals(elem.getLocalName())) { return false; // security header already present. invalid request. } else if(element != null && WSConstants.WSSE_LN.equals(element.getLocalName())){ return false; } } } return true; } /** * Utility method to return basic auth transport headers if present * * @return */ private String getBasicAuthHeaders(MessageContext msgCtx) { Map map = (Map) msgCtx.getProperty(MessageContext.TRANSPORT_HEADERS); if (map == null) { return null; } String tmp = (String) map.get("Authorization"); if (tmp == null) { tmp = (String) map.get("authorization"); } if (tmp != null && tmp.trim().startsWith("Basic ")) { return tmp; } else { return null; } } @Override public void flowComplete(MessageContext msgContext) { // Do Nothing } @Override /** * @see org.apache.axis2.engine.Handler#getHandlerDesc() */ public HandlerDescription getHandlerDesc() { return this.description; } @Override /** * @see org.apache.axis2.engine.Handler#getName() */ public String getName() { return "REST/POX Security handler"; } @Override /** * @see org.apache.axis2.engine.Handler#getParameter(java.lang.String) */ public Parameter getParameter(String name) { return this.description.getParameter(name); } /** * Returns the default "POX_ENABLED" cache */ private Cache<String, String> getPOXCache() { CacheManager manager = Caching.getCacheManagerFactory().getCacheManager(POXSecurityHandler.POX_CACHE_MANAGER); Cache<String, String> cache = manager.getCache(POXSecurityHandler.POX_ENABLED); return cache; } }