/* * JBoss, Home of Professional Open Source * * Copyright 2013 Red Hat, Inc. and/or its affiliates. * * Licensed 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.picketlink.identity.federation.core.wstrust.handlers; import org.picketlink.common.ErrorCodes; import org.picketlink.common.PicketLinkLogger; import org.picketlink.common.PicketLinkLoggerFactory; import org.picketlink.common.exceptions.ParsingException; import org.picketlink.common.exceptions.fed.WSTrustException; import org.picketlink.identity.federation.core.wstrust.STSClient; import org.picketlink.identity.federation.core.wstrust.STSClientConfig; import org.picketlink.identity.federation.core.wstrust.STSClientPool; import org.picketlink.identity.federation.core.wstrust.STSClientFactory; import org.w3c.dom.Element; import javax.annotation.PostConstruct; import javax.annotation.Resource; import javax.xml.namespace.QName; import javax.xml.soap.SOAPException; import javax.xml.soap.SOAPFactory; import javax.xml.soap.SOAPFault; import javax.xml.soap.SOAPHeader; import javax.xml.soap.SOAPHeaderElement; import javax.xml.ws.WebServiceException; import javax.xml.ws.handler.MessageContext; import javax.xml.ws.handler.soap.SOAPHandler; import javax.xml.ws.handler.soap.SOAPMessageContext; import javax.xml.ws.soap.SOAPFaultException; import java.util.Collections; import java.util.Iterator; import java.util.Set; import static org.picketlink.common.constants.WSTrustConstants.FAILED_AUTHENTICATION; import static org.picketlink.common.constants.WSTrustConstants.INVALID_SECURITY; import static org.picketlink.common.constants.WSTrustConstants.SECURITY_TOKEN_UNAVAILABLE; /** * STSSecurityHandler is a server-side JAX-WS SOAP Protocol handler that will extract a Security Token from the SOAP * Security * Header and validate the token with the configured Security Token Service (STS). * <p/> * * This class is abstract to simpify is usage as the intention is for a handler to be specified in a server side * handler * chain. * Here different Security Header specifications and security token specifications can be specified using class names * instead of * using properties which would force users to finding and setting the correct namespaces. Hopefully this will be * easier * and * less error prone. * * <h3>Concrete implementations</h3> * Subclasses a required to implement two methods: * <ul> * <li> {@link #getSecurityElementQName()} This should return the qualified name of the security header. This lets us * support * different versions.</li> * * <li>{@link #getTokenElementQName()} This should return the qualified name of the security token element that should * exist in * the security header. This lets us support different tokens that can be validated with the configured STS.</li> * </ul> * * <h3>Configuration</h3> * handlerchain.xml example: * * <pre> * {@code * <?xml version="1.0" encoding="UTF-8"?> * <jws:handler-config xmlns:jws="http://java.sun.com/xml/ns/javaee"> * <jws:handler-chains> * <jws:handler-chain> * <jws:handler> * <jws:handler-class>org.picketlink.identity.federation.core.wstrust.handlers.STSSaml20Handler</jws:handler-class> * </jws:handler> * </jws:handler-chain> * </jws:handler-chains> * </jws:handler-config> * } * </pre> * <p/> * * This class uses {@link STSClient} to interact with an STS. By default the configuration properties are set in a file * named * {@link STSClientConfig#DEFAULT_CONFIG_FILE}. This can be overridden by specifying environment entries in a * deployment * descriptor. * * For example in web.xml: * * <pre> * {@code * <env-entry> * <env-entry-name>STSClientConfig</env-entry-name> * <env-entry-type>java.lang.String</env-entry-type> * <env-entry-value>/sts-client.properties</env-entry-value> * </env-entry> * } * </pre> * * Username and password for the STS can be configured as shown above in the sts-client.properties file. But it may * also * be * specified by a handler earlier in the handlerchain. Such a handler is expected to extract the username and password * for the * desired location and put these values into the SOAPMessageContext using: <br/> * {@link #USERNAME_MSG_CONTEXT_PROPERTY} <br/> * {@link #PASSWORD_MSG_CONTEXT_PROPERTY} <br/> * These will then be used when contacting the STS, overriding any such values that were parsed from the configuration * file. * * @author <a href="mailto:dbevenius@jboss.com">Daniel Bevenius</a> */ public abstract class STSSecurityHandler implements SOAPHandler<SOAPMessageContext> { protected static final PicketLinkLogger logger = PicketLinkLoggerFactory.getLogger(); /** * Constant that can be used by handlers to set the username in the SOAPMessageContext. */ public static final String USERNAME_MSG_CONTEXT_PROPERTY = "org.picketlink.identity.federation.core.wstrust.handlers.username"; /** * Constant that can be used by handlers to set the password in the SOAPMessageContext. */ public static final String PASSWORD_MSG_CONTEXT_PROPERTY = "org.picketlink.identity.federation.core.wstrust.handlers.password"; /** * The path to the jboss-sts-client.properties file. */ private String configFile = STSClientConfig.DEFAULT_CONFIG_FILE; /** * The STSClient configuration builder. */ private STSClientConfig.Builder configBuilder; /** * Subclasses can return the QName of the Security header element in usage. * * @return QName */ public abstract QName getSecurityElementQName(); /** * Subclasses can return the QName of the Security Element that should be used as the token for validation. * * @return QName */ public abstract QName getTokenElementQName(); /** * Post construct will be called when the handler is deployed. * * @throws WebServiceException */ @PostConstruct public void parseSTSConfig() { configBuilder = new STSClientConfig.Builder(configFile); } /** * Will process in-bound messages and extract a security token from the SOAP Header. This token will then be validated using * by calling the STS.. * * @param messageContext The {@link SOAPMessageContext messageContext}. * @return true If the security token was correctly validated or if this call was an outbound message. * @throws WebServiceException If the security token could not be validated. */ public boolean handleMessage(final SOAPMessageContext messageContext) { if (isOutBound(messageContext)) return true; STSClient stsClient = null; try { final Element securityToken = extractSecurityToken(messageContext, getSecurityElementQName(), getTokenElementQName()); if (securityToken == null) { throwSecurityTokenUnavailable(); } setUsernameFromMessageContext(messageContext, configBuilder); setPasswordFromMessageContext(messageContext, configBuilder); STSClientConfig stsClientConfig = configBuilder.build(); stsClient = createSTSClient(stsClientConfig); if (stsClient.validateToken(securityToken) == false) { throwFailedAuthentication(); } } catch (final WSTrustException e) { throwInvalidSecurity(); } catch (ParsingException e) { throwInvalidSecurity(); } finally { if (stsClient != null) { STSClientPool pool = STSClientFactory.getInstance(); if (pool != null) { pool.returnClient(stsClient); } } } return true; } @SuppressWarnings({"rawtypes"}) private Element extractSecurityToken(final SOAPMessageContext messageContext, final QName securityQName, final QName tokenQName) { try { if (securityQName == null) throw logger.nullArgumentError("securityQName from subclass"); if (tokenQName == null) throw logger.nullArgumentError("tokenQName from subclass"); final SOAPHeader soapHeader = messageContext.getMessage().getSOAPHeader(); final Iterator securityHeaders = soapHeader.getChildElements(securityQName); while (securityHeaders.hasNext()) { final SOAPHeaderElement elem = (SOAPHeaderElement) securityHeaders.next(); // Check if the header is equal to the one this Handler is configured for. if (elem.getElementQName().equals(securityQName)) { final Iterator childElements = elem.getChildElements(tokenQName); while (childElements.hasNext()) { return (Element) childElements.next(); } } } } catch (final SOAPException e) { throwInvalidSecurity(); } return null; } private void throwSecurityTokenUnavailable() throws SOAPFaultException { SOAPFault soapFault = createSoapFault(ErrorCodes.NULL_VALUE + "No security token could be found in the SOAP Header", SECURITY_TOKEN_UNAVAILABLE); throw new SOAPFaultException(soapFault); } private void throwFailedAuthentication() throws SOAPFaultException { SOAPFault soapFault = createSoapFault("The security token could not be authenticated or authorized", FAILED_AUTHENTICATION); throw new SOAPFaultException(soapFault); } private void throwInvalidSecurity() throws SOAPFaultException { SOAPFault soapFault = createSoapFault("An error occurred while processing the security header", INVALID_SECURITY); throw new SOAPFaultException(soapFault); } private SOAPFault createSoapFault(final String msg, final QName qname) { try { SOAPFactory soapFactory = SOAPFactory.newInstance(); return soapFactory.createFault(msg, qname); } catch (SOAPException e) { throw new WebServiceException("Exception while trying to create SOAPFault", e); } } /** * If a property was set for the key {@link #USERNAME_MSG_CONTEXT_PROPERTY} it will be retrieved by this method and set on * the passed-in builder instance. * * @param context The SOAPMessageContext which might contain a username property. * @param builder The STSClientConfigBuilder which be updated if the SOAPMessageContext contains the username property. */ private void setUsernameFromMessageContext(final SOAPMessageContext context, final STSClientConfig.Builder builder) { final String username = (String) context.get(USERNAME_MSG_CONTEXT_PROPERTY); if (username != null) configBuilder.username(username); } /** * If a property was set for the key {@link #PASSWORD_MSG_CONTEXT_PROPERTY} it will be retrieved by this method and set on * the passed-in builder instance. * * @param context The SOAPMessageContext which might contain a password property. * @param builder The STSClientConfigBuilder which be updated if the SOAPMessageContext contains the password property. */ private void setPasswordFromMessageContext(final SOAPMessageContext context, final STSClientConfig.Builder builder) { final String password = (String) context.get(PASSWORD_MSG_CONTEXT_PROPERTY); if (password != null) configBuilder.password(password); } public Set<QName> getHeaders() { return Collections.singleton(getSecurityElementQName()); } public boolean handleFault(final SOAPMessageContext messageContext) { return true; } public void close(final MessageContext messageContext) { // NoOp. } /** * This setter enables the injection of the jboss-sts-client.properties file path. * * @param configFile */ @Resource(name = "STSClientConfig") public void setConfigFile(final String configFile) { if (configFile != null) { this.configFile = configFile; } } STSClientConfig.Builder getConfigBuilder() { return configBuilder; } STSClient createSTSClient(final STSClientConfig config) throws ParsingException { STSClientPool pool = STSClientFactory.getInstance(); if (pool.configExists(config) == false) { pool.createPool(config); } return pool.getClient(config); } private boolean isOutBound(final SOAPMessageContext messageContext) { return ((Boolean) messageContext.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY)).booleanValue(); } }